@tyvm/knowhow 0.0.108 → 0.0.109-dev.e88af1e

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 (214) hide show
  1. package/README.md +45 -0
  2. package/package.json +9 -4
  3. package/scripts/build-for-node.sh +10 -24
  4. package/scripts/publish.sh +86 -0
  5. package/src/agents/base/base.ts +10 -0
  6. package/src/agents/tools/execCommand.ts +49 -6
  7. package/src/agents/tools/index.ts +0 -1
  8. package/src/agents/tools/list.ts +0 -2
  9. package/src/chat/CliChatService.ts +10 -1
  10. package/src/chat/modules/AgentModule.ts +61 -31
  11. package/src/chat/modules/SessionsModule.ts +47 -3
  12. package/src/chat/renderer/CompactRenderer.ts +20 -0
  13. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  14. package/src/chat/renderer/FancyRenderer.ts +19 -0
  15. package/src/chat/renderer/types.ts +11 -0
  16. package/src/cli.ts +91 -659
  17. package/src/clients/anthropic.ts +17 -16
  18. package/src/clients/index.ts +6 -5
  19. package/src/clients/types.ts +19 -4
  20. package/src/cloudWorker.ts +175 -113
  21. package/src/commands/agent.ts +246 -0
  22. package/src/commands/misc.ts +174 -0
  23. package/src/commands/modules.ts +217 -0
  24. package/src/commands/services.ts +77 -0
  25. package/src/commands/workers.ts +168 -0
  26. package/src/config.ts +37 -0
  27. package/src/fileSync.ts +70 -29
  28. package/src/hashes.ts +35 -13
  29. package/src/index.ts +18 -0
  30. package/src/logger.ts +197 -0
  31. package/src/plugins/embedding.ts +11 -6
  32. package/src/plugins/plugins.ts +0 -21
  33. package/src/plugins/vim.ts +5 -16
  34. package/src/processors/JsonCompressor.ts +6 -6
  35. package/src/services/EventService.ts +61 -1
  36. package/src/services/KnowhowClient.ts +34 -4
  37. package/src/services/modules/index.ts +95 -51
  38. package/src/services/modules/types.ts +6 -0
  39. package/src/tunnel.ts +216 -0
  40. package/src/types.ts +0 -1
  41. package/src/worker.ts +105 -312
  42. package/src/workers/auth/WsMiddleware.ts +99 -0
  43. package/src/workers/auth/authMiddleware.ts +104 -0
  44. package/src/workers/auth/types.ts +14 -2
  45. package/src/workers/tools/index.ts +2 -0
  46. package/src/workers/tools/reloadConfig.ts +84 -0
  47. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  48. package/tests/unit/commands/github-credentials.test.ts +211 -0
  49. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  50. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  51. package/ts_build/package.json +9 -4
  52. package/ts_build/src/agents/base/base.js +11 -0
  53. package/ts_build/src/agents/base/base.js.map +1 -1
  54. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  55. package/ts_build/src/agents/tools/execCommand.js +39 -5
  56. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  57. package/ts_build/src/agents/tools/index.d.ts +0 -1
  58. package/ts_build/src/agents/tools/index.js +0 -1
  59. package/ts_build/src/agents/tools/index.js.map +1 -1
  60. package/ts_build/src/agents/tools/list.js +0 -2
  61. package/ts_build/src/agents/tools/list.js.map +1 -1
  62. package/ts_build/src/chat/CliChatService.js +13 -1
  63. package/ts_build/src/chat/CliChatService.js.map +1 -1
  64. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  65. package/ts_build/src/chat/modules/AgentModule.js +43 -20
  66. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  67. package/ts_build/src/chat/modules/SessionsModule.js +37 -3
  68. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  69. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  70. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  71. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  72. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  73. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  74. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  75. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  76. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  77. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  78. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  79. package/ts_build/src/cli.js +47 -519
  80. package/ts_build/src/cli.js.map +1 -1
  81. package/ts_build/src/clients/anthropic.d.ts +5 -5
  82. package/ts_build/src/clients/anthropic.js +17 -16
  83. package/ts_build/src/clients/anthropic.js.map +1 -1
  84. package/ts_build/src/clients/index.js +2 -4
  85. package/ts_build/src/clients/index.js.map +1 -1
  86. package/ts_build/src/clients/types.d.ts +3 -2
  87. package/ts_build/src/cloudWorker.d.ts +14 -0
  88. package/ts_build/src/cloudWorker.js +105 -66
  89. package/ts_build/src/cloudWorker.js.map +1 -1
  90. package/ts_build/src/commands/agent.d.ts +6 -0
  91. package/ts_build/src/commands/agent.js +229 -0
  92. package/ts_build/src/commands/agent.js.map +1 -0
  93. package/ts_build/src/commands/misc.d.ts +10 -0
  94. package/ts_build/src/commands/misc.js +197 -0
  95. package/ts_build/src/commands/misc.js.map +1 -0
  96. package/ts_build/src/commands/modules.d.ts +3 -0
  97. package/ts_build/src/commands/modules.js +207 -0
  98. package/ts_build/src/commands/modules.js.map +1 -0
  99. package/ts_build/src/commands/services.d.ts +5 -0
  100. package/ts_build/src/commands/services.js +87 -0
  101. package/ts_build/src/commands/services.js.map +1 -0
  102. package/ts_build/src/commands/workers.d.ts +6 -0
  103. package/ts_build/src/commands/workers.js +168 -0
  104. package/ts_build/src/commands/workers.js.map +1 -0
  105. package/ts_build/src/config.d.ts +1 -0
  106. package/ts_build/src/config.js +32 -0
  107. package/ts_build/src/config.js.map +1 -1
  108. package/ts_build/src/fileSync.d.ts +6 -0
  109. package/ts_build/src/fileSync.js +50 -23
  110. package/ts_build/src/fileSync.js.map +1 -1
  111. package/ts_build/src/hashes.d.ts +2 -2
  112. package/ts_build/src/hashes.js +35 -9
  113. package/ts_build/src/hashes.js.map +1 -1
  114. package/ts_build/src/index.d.ts +1 -0
  115. package/ts_build/src/index.js +17 -1
  116. package/ts_build/src/index.js.map +1 -1
  117. package/ts_build/src/logger.d.ts +21 -0
  118. package/ts_build/src/logger.js +106 -0
  119. package/ts_build/src/logger.js.map +1 -0
  120. package/ts_build/src/plugins/embedding.js +4 -3
  121. package/ts_build/src/plugins/embedding.js.map +1 -1
  122. package/ts_build/src/plugins/plugins.d.ts +0 -2
  123. package/ts_build/src/plugins/plugins.js +0 -11
  124. package/ts_build/src/plugins/plugins.js.map +1 -1
  125. package/ts_build/src/plugins/vim.js +3 -9
  126. package/ts_build/src/plugins/vim.js.map +1 -1
  127. package/ts_build/src/processors/JsonCompressor.js +4 -4
  128. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  129. package/ts_build/src/services/EventService.d.ts +6 -1
  130. package/ts_build/src/services/EventService.js +29 -0
  131. package/ts_build/src/services/EventService.js.map +1 -1
  132. package/ts_build/src/services/KnowhowClient.d.ts +13 -1
  133. package/ts_build/src/services/KnowhowClient.js +19 -2
  134. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  135. package/ts_build/src/services/modules/index.d.ts +33 -0
  136. package/ts_build/src/services/modules/index.js +67 -47
  137. package/ts_build/src/services/modules/index.js.map +1 -1
  138. package/ts_build/src/services/modules/types.d.ts +6 -0
  139. package/ts_build/src/tunnel.d.ts +27 -0
  140. package/ts_build/src/tunnel.js +112 -0
  141. package/ts_build/src/tunnel.js.map +1 -0
  142. package/ts_build/src/types.d.ts +0 -1
  143. package/ts_build/src/types.js.map +1 -1
  144. package/ts_build/src/worker.d.ts +1 -4
  145. package/ts_build/src/worker.js +59 -227
  146. package/ts_build/src/worker.js.map +1 -1
  147. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  148. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  149. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  150. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  151. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  152. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  153. package/ts_build/src/workers/auth/types.d.ts +8 -1
  154. package/ts_build/src/workers/tools/index.d.ts +2 -0
  155. package/ts_build/src/workers/tools/index.js +4 -1
  156. package/ts_build/src/workers/tools/index.js.map +1 -1
  157. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  158. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  159. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  160. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  161. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  162. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
  163. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  164. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  165. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  166. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  167. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  168. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  169. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  170. package/src/agents/tools/executeScript/README.md +0 -94
  171. package/src/agents/tools/executeScript/definition.ts +0 -79
  172. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  173. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  174. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  175. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  176. package/src/agents/tools/executeScript/index.ts +0 -98
  177. package/src/services/script-execution/SandboxContext.ts +0 -282
  178. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  179. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  180. package/src/services/script-execution/ScriptTracer.ts +0 -249
  181. package/src/services/script-execution/types.ts +0 -134
  182. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  183. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  184. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  185. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  186. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  187. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  188. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  189. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  190. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  191. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  192. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  193. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  194. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  195. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  196. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  197. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  198. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  199. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  200. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  201. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  202. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  203. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  204. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  205. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  206. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  207. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  208. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  209. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  210. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  211. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  212. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  213. package/ts_build/src/services/script-execution/types.js +0 -3
  214. package/ts_build/src/services/script-execution/types.js.map +0 -1
package/src/logger.ts ADDED
@@ -0,0 +1,197 @@
1
+ import type { LogLevel } from "./services/EventService";
2
+
3
+ /**
4
+ * App-wide logger utility.
5
+ *
6
+ * Features:
7
+ * 1. `logger.info/warn/error(source, message)` — routes through EventService
8
+ * 2. `Logger.of("ClassName")` — creates a bound logger so you don't repeat the source
9
+ * 3. `logger.installConsoleOverload()` — replaces console.log/warn/error/info with
10
+ * our closure, so ALL output (including third-party modules) goes through us
11
+ * 4. `logger.silence()` / `logger.unsilence()` — suppress all output, useful for
12
+ * commands that need clean stdout (e.g. github-credentials)
13
+ *
14
+ * Usage (module-level):
15
+ * import { logger } from "../logger";
16
+ * logger.info("MyService", "Something happened");
17
+ *
18
+ * Usage (class-level):
19
+ * import { Logger } from "../logger";
20
+ * class MyClass {
21
+ * private logger = Logger.of("MyClass");
22
+ * doThing() { this.logger.info("Something happened"); }
23
+ * }
24
+ *
25
+ * Silence mode (for clean-stdout commands):
26
+ * logger.silence(); // suppress everything
27
+ * // ... do work that must produce clean stdout ...
28
+ * logger.unsilence(); // restore
29
+ */
30
+
31
+ // ---- Internal state ---------------------------------------------------------
32
+
33
+ let silenced = false;
34
+
35
+ // Original console methods — saved before any overload is installed
36
+ const _originalConsole = {
37
+ log: console.log.bind(console),
38
+ warn: console.warn.bind(console),
39
+ error: console.error.bind(console),
40
+ info: console.info.bind(console),
41
+ };
42
+
43
+ let consoleOverloadInstalled = false;
44
+
45
+ // ---- EventService lazy accessor ---------------------------------------------
46
+
47
+ function getEvents() {
48
+ try {
49
+ const { services } = require("./services") as typeof import("./services");
50
+ return services().Events;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ // ---- Core emit logic --------------------------------------------------------
57
+
58
+ function emit(source: string, message: string, level: LogLevel): void {
59
+ if (silenced) return;
60
+
61
+ try {
62
+ const events = getEvents();
63
+ if (events) {
64
+ events.log(source, message, level);
65
+ return;
66
+ }
67
+ } catch {
68
+ // fall through to direct console output
69
+ }
70
+
71
+ // Fallback: use original console methods (bypasses any overload we installed)
72
+ const prefix = source ? `[${source}] ` : "";
73
+ if (level === "warn") _originalConsole.warn(`${prefix}${message}`);
74
+ else if (level === "error") _originalConsole.error(`${prefix}${message}`);
75
+ else _originalConsole.log(`${prefix}${message}`);
76
+ }
77
+
78
+ // ---- Bound logger (returned by Logger.of) -----------------------------------
79
+
80
+ export interface BoundLogger {
81
+ log(message: string, level?: LogLevel): void;
82
+ info(message: string): void;
83
+ warn(message: string): void;
84
+ error(message: string): void;
85
+ }
86
+
87
+ function makeBoundLogger(source: string): BoundLogger {
88
+ return {
89
+ log(message: string, level: LogLevel = "info"): void {
90
+ emit(source, message, level);
91
+ },
92
+ info(message: string): void {
93
+ emit(source, message, "info");
94
+ },
95
+ warn(message: string): void {
96
+ emit(source, message, "warn");
97
+ },
98
+ error(message: string): void {
99
+ emit(source, message, "error");
100
+ },
101
+ };
102
+ }
103
+
104
+ // ---- Public API -------------------------------------------------------------
105
+
106
+ export const logger = {
107
+ log(source: string, message: string, level: LogLevel = "info"): void {
108
+ emit(source, message, level);
109
+ },
110
+
111
+ info(source: string, message: string): void {
112
+ emit(source, message, "info");
113
+ },
114
+
115
+ warn(source: string, message: string): void {
116
+ emit(source, message, "warn");
117
+ },
118
+
119
+ error(source: string, message: string): void {
120
+ emit(source, message, "error");
121
+ },
122
+
123
+ /**
124
+ * Suppress all log output. Useful for commands that need clean stdout
125
+ * (e.g. git credential helpers). All logger.* calls and overloaded
126
+ * console.* calls become no-ops until unsilence() is called.
127
+ */
128
+ silence(): void {
129
+ silenced = true;
130
+ },
131
+
132
+ /**
133
+ * Restore log output after a silence() call.
134
+ */
135
+ unsilence(): void {
136
+ silenced = false;
137
+ },
138
+
139
+ /**
140
+ * Returns true if the logger is currently silenced.
141
+ */
142
+ isSilenced(): boolean {
143
+ return silenced;
144
+ },
145
+
146
+ /**
147
+ * Install console overload. After this call, console.log/warn/error/info
148
+ * all route through our closure (respecting silence mode).
149
+ * Safe to call multiple times — only installs once.
150
+ *
151
+ * Call this early in CLI startup (before any modules are loaded) to ensure
152
+ * third-party module logs don't bypass the silence mechanism.
153
+ */
154
+ installConsoleOverload(): void {
155
+ if (consoleOverloadInstalled) return;
156
+ consoleOverloadInstalled = true;
157
+
158
+ const route = (originalFn: (...args: any[]) => void, args: any[]) => {
159
+ if (silenced) return;
160
+ originalFn(...args);
161
+ };
162
+
163
+ console.log = (...args: any[]) => route(_originalConsole.log, args);
164
+ console.info = (...args: any[]) => route(_originalConsole.info, args);
165
+ console.warn = (...args: any[]) => route(_originalConsole.warn, args);
166
+ // Note: console.error is intentionally NOT overloaded — real errors (stack
167
+ // traces, crash reports) should always be visible. Only suppress via silence().
168
+ // If you want to suppress errors too, call logger.silence() which checks the flag
169
+ // before the overloaded console.warn/log routes reach here anyway.
170
+ },
171
+
172
+ /**
173
+ * Remove the console overload and restore original console methods.
174
+ */
175
+ uninstallConsoleOverload(): void {
176
+ if (!consoleOverloadInstalled) return;
177
+ console.log = _originalConsole.log;
178
+ console.info = _originalConsole.info;
179
+ console.warn = _originalConsole.warn;
180
+ consoleOverloadInstalled = false;
181
+ },
182
+ };
183
+
184
+ /**
185
+ * Factory for creating a bound logger with a fixed source name.
186
+ * Ideal for class-level loggers:
187
+ *
188
+ * class MyClass {
189
+ * private logger = Logger.of("MyClass");
190
+ * doThing() { this.logger.info("hello"); }
191
+ * }
192
+ */
193
+ export const Logger = {
194
+ of(source: string): BoundLogger {
195
+ return makeBoundLogger(source);
196
+ },
197
+ };
@@ -20,9 +20,12 @@ export class EmbeddingPlugin extends PluginBase {
20
20
 
21
21
  constructor(context) {
22
22
  super(context);
23
-
23
+
24
24
  // Subscribe to file:post-edit events
25
- this.context.Events.on("file:post-edit", this.handleFilePostEdit.bind(this));
25
+ this.context.Events.on(
26
+ "file:post-edit",
27
+ this.handleFilePostEdit.bind(this)
28
+ );
26
29
  }
27
30
 
28
31
  async embed() {
@@ -68,10 +71,12 @@ export class EmbeddingPlugin extends PluginBase {
68
71
  this.log(`Reading entry ${entry.id}`);
69
72
  }
70
73
 
71
- const contextLength = JSON.stringify(context).split(" ").length;
74
+ const ids = context.map((entry) => entry.id);
75
+
76
+ const contextLength = JSON.stringify(ids).split(" ").length;
72
77
  this.log(`Found ${context.length} entries. Loading ${contextLength} words`);
73
78
 
74
- return `EMBEDDING PLUGIN: Our knowledgebase contains this information which can be used to answer the question:
75
- ${JSON.stringify(context)}`;
79
+ return `EMBEDDING PLUGIN: Our knowledgebase indicates these embedding entries may be related to the question:
80
+ ${JSON.stringify(ids)}`;
76
81
  }
77
- }
82
+ }
@@ -1,5 +1,4 @@
1
1
  import { Plugin, PluginContext } from "./types";
2
- import { Config } from "../types";
3
2
  import { VimPlugin } from "./vim";
4
3
  import { LinterPlugin } from "./LinterPlugin";
5
4
  import { LanguagePlugin } from "./language";
@@ -49,26 +48,6 @@ export class PluginService {
49
48
  return instance.meta.key;
50
49
  }
51
50
 
52
- /**
53
- * Load plugins from config's pluginPackages map.
54
- * Each entry maps a plugin key to an npm package name or file path.
55
- * Errors are caught and logged as warnings without crashing.
56
- */
57
- async loadPluginsFromConfig(config: Config): Promise<void> {
58
- const pluginPackages = config.pluginPackages || {};
59
- for (const [key, spec] of Object.entries(pluginPackages)) {
60
- try {
61
- await this.loadPlugin(spec);
62
- } catch (error) {
63
- this.events?.log(
64
- "PluginService",
65
- `Failed to load plugin "${key}" from "${spec}": ${error instanceof Error ? error.message : error}`,
66
- "warn"
67
- );
68
- }
69
- }
70
- }
71
-
72
51
  /** Disable a plugin by its key; returns `true` if found. */
73
52
  disablePlugin(key: string): boolean {
74
53
  const p = this.pluginMap.get(key);
@@ -73,22 +73,11 @@ export class VimPlugin extends PluginBase {
73
73
 
74
74
  async call() {
75
75
  const vimFiles = await this.getVimFiles();
76
- const fileContents = await Promise.all(
77
- vimFiles.map(async (f) => {
78
- const loaded = await this.getFileContents(f);
79
-
80
- const preview =
81
- loaded.content.length > 1000
82
- ? loaded.content.slice(0, 1000) +
83
- "... file trimmed, read file for full content"
84
- : loaded.content;
85
-
86
- return {
87
- sourceFile: loaded.filePath,
88
- content: loaded.content.slice(0, 1000),
89
- };
90
- })
91
- );
76
+ const fileContents = vimFiles.map((f) => {
77
+ return {
78
+ sourceFile: f,
79
+ };
80
+ });
92
81
  if (fileContents.length === 0) {
93
82
  return "VIM PLUGIN: No files open in vim";
94
83
  }
@@ -382,7 +382,7 @@ export class JsonCompressor {
382
382
  i + currentChunk.length - 1
383
383
  }]\nPreview: ${chunkString.substring(0, 100)}...\n[Use ${
384
384
  this.toolName
385
- } tool with key "${key}" to retrieve this chunk]`;
385
+ } tool with key "${key}" to retrieve this chunk]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
386
386
  finalArray.unshift(stub); // Add stub to the start of our final result.
387
387
 
388
388
  currentChunk = [];
@@ -415,11 +415,11 @@ export class JsonCompressor {
415
415
 
416
416
  // Store objects on FIRST occurrence so second occurrence can reference it
417
417
  // We increment seenCount above, so after increment:
418
- // seenCount=1: first occurrence (just incremented from 0 to 1), store it
419
- // seenCount>=2: we already stored it on first occurrence, should be in dedup map
418
+ // seenCount=0: first occurrence (before increment), store it
419
+ // seenCount>=1: we already stored it on first occurrence, should be in dedup map
420
420
  // Note: This means we store proactively - first occurrence gets stored AND returned in full
421
421
  // Second+ occurrences will find it in the dedup map and return a reference
422
- const isFirstOccurrence = seenCount === 1;
422
+ const isFirstOccurrence = seenCount === 0;
423
423
 
424
424
  // Process the object - apply low-signal detection
425
425
  const objWithLowSignalCompressed = this.compressObjectWithLowSignalDetection(obj, path);
@@ -453,7 +453,7 @@ export class JsonCompressor {
453
453
  result
454
454
  ).join(", ")}\nPreview: ${objectAsString.substring(0, 200)}...\n[Use ${
455
455
  this.toolName
456
- } tool with key "${key}" to retrieve full content]`;
456
+ } tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
457
457
  }
458
458
  return result;
459
459
  }
@@ -486,7 +486,7 @@ export class JsonCompressor {
486
486
  200
487
487
  )}...\n[Use ${
488
488
  this.toolName
489
- } tool with key "${key}" to retrieve full content]`;
489
+ } tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
490
490
  }
491
491
  return obj;
492
492
  }
@@ -1,6 +1,8 @@
1
1
  import { EventEmitter } from "events";
2
2
  import { IAgent } from "../agents/interface";
3
3
 
4
+ export type LogLevel = "info" | "warn" | "error";
5
+
4
6
  export type EventHandlerFn = (...args: any[]) => any;
5
7
 
6
8
  export interface EventHandler {
@@ -31,9 +33,38 @@ type ManagedListenerRecord = {
31
33
  blocking: boolean;
32
34
  };
33
35
 
36
+ /**
37
+ * Default console handler for plugin:log events.
38
+ * Active when no renderer has taken over (e.g. worker mode, CLI before chat starts).
39
+ * Can be suppressed by calling suppressDefaultLogger() when a renderer is active.
40
+ *
41
+ * IMPORTANT: Uses process.stdout/stderr directly to avoid infinite recursion
42
+ * with logger.installConsoleOverload() which overrides console.log/warn.
43
+ */
44
+ function defaultConsoleLogHandler(event: {
45
+ source: string;
46
+ message: string;
47
+ level: LogLevel;
48
+ }): void {
49
+ const prefix = event.source ? `[${event.source}] ` : "";
50
+ const line = `${prefix}${event.message}\n`;
51
+ switch (event.level) {
52
+ case "warn":
53
+ process.stderr.write(line);
54
+ break;
55
+ case "error":
56
+ process.stderr.write(line);
57
+ break;
58
+ default:
59
+ process.stdout.write(line);
60
+ }
61
+ }
62
+
34
63
  export class EventService extends EventEmitter {
35
64
  private blockingHandlers: Map<string, EventHandler[]> = new Map();
36
65
  private managedListeners: Map<string, ManagedListenerRecord> = new Map();
66
+ private defaultLoggerActive = true;
67
+ private boundDefaultLogHandler = defaultConsoleLogHandler;
37
68
 
38
69
  eventTypes = {
39
70
  agentMsg: "agent:msg",
@@ -45,6 +76,35 @@ export class EventService extends EventEmitter {
45
76
  constructor() {
46
77
  super();
47
78
  this.setMaxListeners(100);
79
+ // Register the default console logger so Events.log() always produces output
80
+ // even before a renderer is attached (worker mode, module loading, etc.)
81
+ this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
82
+ }
83
+
84
+ /**
85
+ * Suppress the default console logger.
86
+ * Call this when a renderer has taken over and will handle plugin:log events.
87
+ * This prevents double-printing when both the renderer and the default handler fire.
88
+ */
89
+ suppressDefaultLogger(): void {
90
+ if (this.defaultLoggerActive) {
91
+ this.removeListener(
92
+ this.eventTypes.pluginLog,
93
+ this.boundDefaultLogHandler
94
+ );
95
+ this.defaultLoggerActive = false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Restore the default console logger.
101
+ * Call this when the renderer is torn down.
102
+ */
103
+ restoreDefaultLogger(): void {
104
+ if (!this.defaultLoggerActive) {
105
+ this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
106
+ this.defaultLoggerActive = true;
107
+ }
48
108
  }
49
109
 
50
110
  /**
@@ -232,7 +292,7 @@ export class EventService extends EventEmitter {
232
292
  log(
233
293
  source: string,
234
294
  message: string,
235
- level: "info" | "warn" | "error" = "info"
295
+ level: LogLevel = "info"
236
296
  ): void {
237
297
  this.emit(this.eventTypes.pluginLog, {
238
298
  source,
@@ -1,3 +1,4 @@
1
+ import { createHash } from "crypto";
1
2
  import http from "../utils/http";
2
3
  import fs from "fs";
3
4
  import { Message } from "../clients/types";
@@ -171,11 +172,9 @@ export class KnowhowSimpleClient {
171
172
  try {
172
173
  this.jwtValidated = true;
173
174
  const response = await this.me();
174
-
175
175
  const user = response.data.user;
176
176
  const orgs = user.orgs;
177
177
  const orgId = response.data.orgId;
178
-
179
178
  const currentOrg = orgs.find((org) => {
180
179
  return org.organizationId === orgId;
181
180
  });
@@ -227,6 +226,17 @@ export class KnowhowSimpleClient {
227
226
  return presignedUrl;
228
227
  }
229
228
 
229
+ async getOrgEmbedding(id: string) {
230
+ await this.checkJwt();
231
+ const resp = await http.get(
232
+ `${this.baseUrl}/api/org-embeddings/${id}`,
233
+ {
234
+ headers: this.headers,
235
+ }
236
+ );
237
+ return resp.data as { id: string; modelName: string; name: string; [key: string]: unknown };
238
+ }
239
+
230
240
  async updateEmbeddingMetadata(
231
241
  id: string,
232
242
  data: {
@@ -654,8 +664,10 @@ export class KnowhowSimpleClient {
654
664
  /**
655
665
  * Get presigned S3 URL for uploading a file to Knowhow FS.
656
666
  * First finds or creates the file by path, then gets its upload URL.
667
+ * Computes SHA256 hash of the file content and stores it as S3 metadata
668
+ * so any client can determine if they already have this version without downloading.
657
669
  */
658
- async getOrgFilePresignedUploadUrl(filePath: string): Promise<string> {
670
+ async getOrgFilePresignedUploadUrl(filePath: string, localFilePath?: string): Promise<string> {
659
671
  await this.checkJwt();
660
672
 
661
673
  // Find or create the file by path
@@ -666,10 +678,17 @@ export class KnowhowSimpleClient {
666
678
  const fileName =
667
679
  lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
668
680
 
681
+ // Compute SHA256 hash if we have the local file path, so S3 stores it as metadata
682
+ let sha256Hash: string | undefined;
683
+ if (localFilePath) {
684
+ const fileContent = fs.readFileSync(localFilePath);
685
+ sha256Hash = createHash("sha256").update(fileContent).digest("base64");
686
+ }
687
+
669
688
  // Get upload URL using the file ID
670
689
  const response = await http.post<{ uploadUrl: string }>(
671
690
  `${this.baseUrl}/api/org-files/upload/${file.id}`,
672
- { fileName },
691
+ { fileName, sha256Hash },
673
692
  { headers: this.headers }
674
693
  );
675
694
  return response.data.uploadUrl;
@@ -718,6 +737,17 @@ export class KnowhowSimpleClient {
718
737
  );
719
738
  }
720
739
 
740
+ /**
741
+ * Get a single cloud worker by ID
742
+ */
743
+ async getCloudWorker(id: string) {
744
+ await this.checkJwt();
745
+ return http.get<{ id: string; name: string; status: string; workerConfigJson?: Record<string, unknown> }>(
746
+ `${this.baseUrl}/api/cloud-workers/${id}`,
747
+ { headers: this.headers }
748
+ );
749
+ }
750
+
721
751
  /**
722
752
  * Update an existing cloud worker
723
753
  */
@@ -1,82 +1,126 @@
1
+ import * as path from "path";
2
+ import * as os from "os";
3
+
1
4
  import { getConfig, getGlobalConfig } from "../../config";
2
5
  import { KnowhowModule, ModuleContext } from "./types";
3
- import { ToolsService } from "../Tools";
4
6
  import { services } from "../";
5
- import * as path from "path";
7
+ import { toUniqueArray } from "../../utils";
6
8
 
7
9
  export class ModulesService {
8
- async loadModulesFromConfig(context?: ModuleContext) {
9
- const config = await getConfig();
10
+ async getDefaultContext() {
11
+ return { ...services() };
12
+ }
10
13
 
14
+ async overrideDefaultContext(overrides: Partial<ModuleContext>) {
15
+ const defaultContext = await this.getDefaultContext();
16
+ return { ...defaultContext, ...overrides };
17
+ }
18
+
19
+ async loadModulesFrom(
20
+ config: { modules: string[] } & any,
21
+ context?: Partial<ModuleContext>
22
+ ) {
11
23
  // If no context provided, fall back to global singletons
12
24
  if (!context) {
13
- const { Clients, Plugins, Agents, Tools, Embeddings, MediaProcessor } = services();
14
- context = {
15
- Agents,
16
- Embeddings,
17
- Plugins,
18
- Clients,
19
- Tools,
20
- MediaProcessor,
21
- };
25
+ context = { ...(await this.getDefaultContext()) };
22
26
  }
23
27
 
24
- // Use the toolsService from context
25
- const toolsService = context.Tools;
26
- const agentService = context.Agents;
27
- const pluginService = context.Plugins;
28
- const clients = context.Clients;
28
+ const allModulePaths = config.modules;
29
29
 
30
- // Load from global config (~/.knowhow/knowhow.json) first, then local config
31
- const globalConfig = await getGlobalConfig();
32
- const allModulePaths = [
33
- ...(globalConfig.modules || []),
34
- ...(config.modules || []),
30
+ // Search paths: .knowhow/node_modules first (where `knowhow modules install`
31
+ // puts packages), then cwd node_modules, then global node_modules.
32
+ // This allows modules installed via `knowhow modules install` to be found
33
+ // even when knowhow itself is installed globally.
34
+ const cwdPaths = (require as any).resolve
35
+ ? require.resolve.paths?.("") || []
36
+ : [];
37
+ const resolvePaths = [
38
+ path.join(process.cwd(), ".knowhow", "node_modules"),
39
+ path.join(os.homedir(), ".knowhow", "node_modules"),
40
+ path.join(process.cwd(), "node_modules"),
41
+ ...cwdPaths,
35
42
  ];
36
43
 
37
44
  for (const modulePath of allModulePaths) {
38
45
  // Resolve relative paths relative to process.cwd() so that paths like
39
46
  // "../../packages/knowhow-module-load-webpage" in knowhow.json work
40
47
  // regardless of where the compiled output lives.
41
- const resolvedPath = modulePath.startsWith(".")
42
- ? path.resolve(process.cwd(), modulePath)
43
- : modulePath;
48
+ let resolvedPath: string;
49
+ if (modulePath.startsWith(".")) {
50
+ resolvedPath = path.resolve(process.cwd(), modulePath);
51
+ } else {
52
+ // For npm package names, try resolving from cwd first so locally-installed
53
+ // modules are found even when knowhow is installed globally.
54
+ try {
55
+ resolvedPath = require.resolve(modulePath, { paths: resolvePaths });
56
+ } catch {
57
+ resolvedPath = modulePath; // fall back to normal require resolution
58
+ }
59
+ }
44
60
  const rawModule = require(resolvedPath);
45
61
  const importedModule = (rawModule.default || rawModule) as KnowhowModule;
46
- console.log(`🔌 Loading module: ${modulePath} (resolved: ${resolvedPath})`);
47
- await importedModule.init({ config, cwd: process.cwd(), context });
48
- console.log(`✅ Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`);
62
+ context.Events?.log(
63
+ "ModulesService",
64
+ `🔌 Loading module: ${modulePath} (resolved: ${resolvedPath})`
65
+ );
66
+ await importedModule.init({
67
+ config,
68
+ cwd: process.cwd(),
69
+ context: context as ModuleContext,
70
+ });
71
+ context.Events?.log(
72
+ "ModulesService",
73
+ `✅ Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
74
+ );
49
75
 
50
- for (const agent of importedModule.agents) {
51
- agentService.registerAgent(agent);
76
+ // Only register tools/agents/plugins/clients if the relevant services
77
+ // are available in context (they may not be during early CLI command registration)
78
+ if (context.Agents) {
79
+ for (const agent of importedModule.agents) {
80
+ context.Agents.registerAgent(agent);
81
+ }
52
82
  }
53
83
 
54
- for (const tool of importedModule.tools) {
55
- toolsService.addTool(tool.definition);
56
- toolsService.setFunction(tool.definition.function.name, tool.handler);
84
+ if (context.Tools) {
85
+ for (const tool of importedModule.tools) {
86
+ context.Tools.addTool(tool.definition);
87
+ context.Tools.setFunction(
88
+ tool.definition.function.name,
89
+ tool.handler
90
+ );
91
+ }
57
92
  }
58
93
 
59
- for (const plugin of importedModule.plugins) {
60
- const pluginContext = {
61
- Agents: agentService,
62
- Clients: clients,
63
- Tools: toolsService,
64
- Plugins: pluginService,
65
- ...(context.MediaProcessor ? { MediaProcessor: context.MediaProcessor } : {}),
66
- };
67
- pluginService.registerPlugin(plugin.name, new plugin.plugin(pluginContext as any));
94
+ if (context.Plugins) {
95
+ for (const plugin of importedModule.plugins) {
96
+ context.Plugins.registerPlugin(
97
+ plugin.name,
98
+ new plugin.plugin(context as any)
99
+ );
100
+ }
68
101
  }
69
102
 
70
- for (const client of importedModule.clients) {
71
- clients.registerClient(client.provider, client.client);
72
- clients.registerModels(client.provider, client.models);
103
+ if (context.Clients) {
104
+ for (const client of importedModule.clients) {
105
+ context.Clients.registerClient(client.provider, client.client);
106
+ context.Clients.registerModels(client.provider, client.models);
107
+ }
73
108
  }
74
109
  }
110
+ }
75
111
 
76
- // Also load plugins directly from config's pluginPackages map
77
- if (pluginService) {
78
- await pluginService.loadPluginsFromConfig(config);
79
- await pluginService.loadPluginsFromConfig(globalConfig);
80
- }
112
+ async loadModulesFromConfig(context?: ModuleContext) {
113
+ const config = await getConfig();
114
+
115
+ const globalConfig = await getGlobalConfig();
116
+ const allModulePaths = [
117
+ ...(globalConfig.modules || []),
118
+ ...(config.modules || []),
119
+ ];
120
+
121
+ return this.loadModulesFrom(
122
+ { ...config, modules: toUniqueArray(allModulePaths) },
123
+ context
124
+ );
81
125
  }
82
126
  }