@tyvm/knowhow 0.0.108-dev.126b29e → 0.0.108-dev.501f36f

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 (177) hide show
  1. package/package.json +2 -3
  2. package/src/agents/base/base.ts +9 -0
  3. package/src/agents/tools/index.ts +0 -1
  4. package/src/agents/tools/list.ts +0 -2
  5. package/src/chat/CliChatService.ts +10 -1
  6. package/src/chat/renderer/CompactRenderer.ts +20 -0
  7. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  8. package/src/chat/renderer/FancyRenderer.ts +19 -0
  9. package/src/chat/renderer/types.ts +11 -0
  10. package/src/cli.ts +91 -664
  11. package/src/clients/index.ts +6 -5
  12. package/src/clients/types.ts +12 -4
  13. package/src/cloudWorker.ts +110 -122
  14. package/src/commands/agent.ts +246 -0
  15. package/src/commands/misc.ts +174 -0
  16. package/src/commands/modules.ts +182 -0
  17. package/src/commands/services.ts +77 -0
  18. package/src/commands/workers.ts +168 -0
  19. package/src/config.ts +37 -0
  20. package/src/fileSync.ts +50 -17
  21. package/src/index.ts +1 -0
  22. package/src/logger.ts +197 -0
  23. package/src/plugins/plugins.ts +0 -21
  24. package/src/processors/JsonCompressor.ts +6 -6
  25. package/src/services/EventService.ts +61 -1
  26. package/src/services/KnowhowClient.ts +12 -2
  27. package/src/services/S3.ts +0 -10
  28. package/src/services/modules/index.ts +70 -50
  29. package/src/services/modules/types.ts +4 -0
  30. package/src/tunnel.ts +216 -0
  31. package/src/types.ts +0 -1
  32. package/src/worker.ts +65 -336
  33. package/src/workers/auth/WsMiddleware.ts +99 -0
  34. package/src/workers/auth/authMiddleware.ts +104 -0
  35. package/src/workers/auth/types.ts +14 -2
  36. package/tests/unit/commands/github-credentials.test.ts +211 -0
  37. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  38. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  39. package/ts_build/package.json +2 -3
  40. package/ts_build/src/agents/base/base.js +10 -0
  41. package/ts_build/src/agents/base/base.js.map +1 -1
  42. package/ts_build/src/agents/tools/index.d.ts +0 -1
  43. package/ts_build/src/agents/tools/index.js +0 -1
  44. package/ts_build/src/agents/tools/index.js.map +1 -1
  45. package/ts_build/src/agents/tools/list.js +0 -2
  46. package/ts_build/src/agents/tools/list.js.map +1 -1
  47. package/ts_build/src/chat/CliChatService.js +13 -1
  48. package/ts_build/src/chat/CliChatService.js.map +1 -1
  49. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  50. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  51. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  52. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  53. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  54. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  55. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  56. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  57. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  58. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  59. package/ts_build/src/cli.js +47 -525
  60. package/ts_build/src/cli.js.map +1 -1
  61. package/ts_build/src/clients/index.js +2 -4
  62. package/ts_build/src/clients/index.js.map +1 -1
  63. package/ts_build/src/clients/types.d.ts +2 -2
  64. package/ts_build/src/cloudWorker.d.ts +5 -0
  65. package/ts_build/src/cloudWorker.js +69 -66
  66. package/ts_build/src/cloudWorker.js.map +1 -1
  67. package/ts_build/src/commands/agent.d.ts +6 -0
  68. package/ts_build/src/commands/agent.js +229 -0
  69. package/ts_build/src/commands/agent.js.map +1 -0
  70. package/ts_build/src/commands/misc.d.ts +10 -0
  71. package/ts_build/src/commands/misc.js +197 -0
  72. package/ts_build/src/commands/misc.js.map +1 -0
  73. package/ts_build/src/commands/modules.d.ts +3 -0
  74. package/ts_build/src/commands/modules.js +160 -0
  75. package/ts_build/src/commands/modules.js.map +1 -0
  76. package/ts_build/src/commands/services.d.ts +5 -0
  77. package/ts_build/src/commands/services.js +87 -0
  78. package/ts_build/src/commands/services.js.map +1 -0
  79. package/ts_build/src/commands/workers.d.ts +6 -0
  80. package/ts_build/src/commands/workers.js +168 -0
  81. package/ts_build/src/commands/workers.js.map +1 -0
  82. package/ts_build/src/config.d.ts +1 -0
  83. package/ts_build/src/config.js +32 -0
  84. package/ts_build/src/config.js.map +1 -1
  85. package/ts_build/src/fileSync.d.ts +6 -0
  86. package/ts_build/src/fileSync.js +37 -12
  87. package/ts_build/src/fileSync.js.map +1 -1
  88. package/ts_build/src/index.d.ts +1 -0
  89. package/ts_build/src/index.js +3 -1
  90. package/ts_build/src/index.js.map +1 -1
  91. package/ts_build/src/logger.d.ts +21 -0
  92. package/ts_build/src/logger.js +106 -0
  93. package/ts_build/src/logger.js.map +1 -0
  94. package/ts_build/src/plugins/plugins.d.ts +0 -2
  95. package/ts_build/src/plugins/plugins.js +0 -11
  96. package/ts_build/src/plugins/plugins.js.map +1 -1
  97. package/ts_build/src/processors/JsonCompressor.js +4 -4
  98. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  99. package/ts_build/src/services/EventService.d.ts +6 -1
  100. package/ts_build/src/services/EventService.js +29 -0
  101. package/ts_build/src/services/EventService.js.map +1 -1
  102. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  103. package/ts_build/src/services/KnowhowClient.js +8 -2
  104. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  105. package/ts_build/src/services/S3.js +0 -7
  106. package/ts_build/src/services/S3.js.map +1 -1
  107. package/ts_build/src/services/modules/index.d.ts +33 -0
  108. package/ts_build/src/services/modules/index.js +46 -45
  109. package/ts_build/src/services/modules/index.js.map +1 -1
  110. package/ts_build/src/services/modules/types.d.ts +4 -0
  111. package/ts_build/src/tunnel.d.ts +27 -0
  112. package/ts_build/src/tunnel.js +112 -0
  113. package/ts_build/src/tunnel.js.map +1 -0
  114. package/ts_build/src/types.d.ts +0 -1
  115. package/ts_build/src/types.js.map +1 -1
  116. package/ts_build/src/worker.d.ts +1 -4
  117. package/ts_build/src/worker.js +38 -244
  118. package/ts_build/src/worker.js.map +1 -1
  119. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  120. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  121. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  122. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  123. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  124. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  125. package/ts_build/src/workers/auth/types.d.ts +8 -1
  126. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  127. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  128. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  129. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  130. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  131. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  132. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  133. package/src/agents/tools/executeScript/README.md +0 -94
  134. package/src/agents/tools/executeScript/definition.ts +0 -79
  135. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  136. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  137. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  138. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  139. package/src/agents/tools/executeScript/index.ts +0 -98
  140. package/src/services/script-execution/SandboxContext.ts +0 -282
  141. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  142. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  143. package/src/services/script-execution/ScriptTracer.ts +0 -249
  144. package/src/services/script-execution/types.ts +0 -134
  145. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  146. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  147. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  148. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  149. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  150. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  151. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  152. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  153. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  154. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  155. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  156. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  157. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  158. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  159. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  160. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  161. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  162. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  163. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  164. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  165. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  166. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  167. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  168. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  169. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  170. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  171. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  172. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  173. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  174. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  175. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  176. package/ts_build/src/services/script-execution/types.js +0 -3
  177. package/ts_build/src/services/script-execution/types.js.map +0 -1
package/src/fileSync.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import * as os from "os";
3
4
  import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
4
5
  import { loadJwt } from "./login";
5
6
  import { getConfig } from "./config";
@@ -7,6 +8,8 @@ import { services } from "./services";
7
8
  import { S3Service } from "./services/S3";
8
9
  import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
9
10
 
11
+ export const DEFAULT_BATCH_SIZE = 5;
12
+
10
13
  export interface FileSyncOptions {
11
14
  upload?: boolean;
12
15
  download?: boolean;
@@ -15,6 +18,33 @@ export interface FileSyncOptions {
15
18
  dryRun?: boolean;
16
19
  }
17
20
 
21
+ /**
22
+ * Run an array of async tasks in batches of `batchSize` at a time.
23
+ * Returns results in the same order as the input tasks.
24
+ */
25
+ export async function batchRun<T>(
26
+ tasks: (() => Promise<T>)[],
27
+ batchSize: number = DEFAULT_BATCH_SIZE
28
+ ): Promise<T[]> {
29
+ const results: T[] = [];
30
+ for (let i = 0; i < tasks.length; i += batchSize) {
31
+ const batch = tasks.slice(i, i + batchSize);
32
+ const batchResults = await Promise.all(batch.map((t) => t()));
33
+ results.push(...batchResults);
34
+ }
35
+ return results;
36
+ }
37
+
38
+ /**
39
+ * Expands a leading ~ to the user's home directory
40
+ */
41
+ function expandHome(p: string): string {
42
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
43
+ return path.join(os.homedir(), p.slice(1));
44
+ }
45
+ return p;
46
+ }
47
+
18
48
  /**
19
49
  * Returns true if the path looks like a directory (ends with /)
20
50
  */
@@ -25,7 +55,7 @@ function isDirectoryPath(p: string): boolean {
25
55
  /**
26
56
  * Recursively list all files in a local directory, returning relative paths
27
57
  */
28
- function listFilesRecursively(dir: string): string[] {
58
+ export function listFilesRecursively(dir: string): string[] {
29
59
  const results: string[] = [];
30
60
  if (!fs.existsSync(dir)) return results;
31
61
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -83,7 +113,8 @@ export async function fileSync(options: FileSyncOptions = {}) {
83
113
 
84
114
  // Process each file mount
85
115
  for (const mount of config.files) {
86
- const { remotePath, localPath, direction = "download" } = mount;
116
+ const { remotePath, localPath: rawLocalPath, direction = "download" } = mount;
117
+ const localPath = expandHome(rawLocalPath);
87
118
 
88
119
  // Determine actual direction based on flags and config
89
120
  let actualDirection = direction;
@@ -126,11 +157,10 @@ export async function fileSync(options: FileSyncOptions = {}) {
126
157
  }
127
158
  }
128
159
 
129
-
130
160
  /**
131
161
  * Download a file from Knowhow FS to local filesystem
132
162
  */
133
- async function downloadFile(
163
+ export async function downloadFile(
134
164
  client: KnowhowSimpleClient,
135
165
  s3Service: S3Service,
136
166
  remotePath: string,
@@ -186,7 +216,7 @@ async function downloadFile(
186
216
  /**
187
217
  * Upload a file from local filesystem to Knowhow FS
188
218
  */
189
- async function uploadFile(
219
+ export async function uploadFile(
190
220
  client: KnowhowSimpleClient,
191
221
  s3Service: S3Service,
192
222
  remotePath: string,
@@ -215,7 +245,7 @@ async function uploadFile(
215
245
  }
216
246
 
217
247
  // Get presigned upload URL
218
- const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
248
+ const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath, localPath);
219
249
 
220
250
  // Upload file using presigned URL
221
251
  await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
@@ -261,26 +291,28 @@ export async function uploadDirectory(
261
291
 
262
292
  console.log(` Found ${localFiles.length} local file(s)`);
263
293
 
264
- let count = 0;
265
- for (const relFile of localFiles) {
294
+ const tasks = localFiles.map((relFile) => async () => {
266
295
  const localFilePath = localDir + relFile;
267
296
  const remoteFilePath = remoteDir + relFile;
268
297
  try {
269
298
  await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun);
270
- count++;
299
+ return 1;
271
300
  } catch (error) {
272
301
  console.error(
273
302
  ` ❌ Failed to upload ${localFilePath}, skipping: ${error.message}`
274
303
  );
304
+ return 0;
275
305
  }
276
- }
277
- return count;
306
+ });
307
+
308
+ const counts = await batchRun(tasks);
309
+ return counts.reduce((sum, n) => sum + n, 0);
278
310
  }
279
311
 
280
312
  /**
281
313
  * Download all files from a remote directory path to a local directory
282
314
  */
283
- async function downloadDirectory(
315
+ export async function downloadDirectory(
284
316
  client: KnowhowSimpleClient,
285
317
  s3Service: S3Service,
286
318
  remotePath: string,
@@ -313,8 +345,7 @@ async function downloadDirectory(
313
345
 
314
346
  console.log(` Found ${matchingFiles.length} remote file(s)`);
315
347
 
316
- let count = 0;
317
- for (const f of matchingFiles) {
348
+ const tasks = matchingFiles.map((f) => async () => {
318
349
  const fullRemotePath = f.folderPath.endsWith("/")
319
350
  ? f.folderPath + f.fileName
320
351
  : f.folderPath + "/" + f.fileName;
@@ -322,7 +353,9 @@ async function downloadDirectory(
322
353
  const relativePath = fullRemotePath.slice(remoteDir.length);
323
354
  const localFilePath = localDir + relativePath;
324
355
  await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun);
325
- count++;
326
- }
327
- return count;
356
+ return 1;
357
+ });
358
+
359
+ const counts = await batchRun(tasks);
360
+ return counts.reduce((sum, n) => sum + n, 0);
328
361
  }
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export * as ai from "./ai";
53
53
 
54
54
  // Export module system types for external modules
55
55
  export * from "./services/modules/types";
56
+ export { ModulesService } from "./services/modules";
56
57
  // Export plugin types for external plugins
57
58
  export { PluginBase } from "./plugins/PluginBase";
58
59
  export { PluginMeta, Plugin, PluginContext } from "./plugins/types";
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
+ };
@@ -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);
@@ -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";
@@ -663,8 +664,10 @@ export class KnowhowSimpleClient {
663
664
  /**
664
665
  * Get presigned S3 URL for uploading a file to Knowhow FS.
665
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.
666
669
  */
667
- async getOrgFilePresignedUploadUrl(filePath: string): Promise<string> {
670
+ async getOrgFilePresignedUploadUrl(filePath: string, localFilePath?: string): Promise<string> {
668
671
  await this.checkJwt();
669
672
 
670
673
  // Find or create the file by path
@@ -675,10 +678,17 @@ export class KnowhowSimpleClient {
675
678
  const fileName =
676
679
  lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
677
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
+
678
688
  // Get upload URL using the file ID
679
689
  const response = await http.post<{ uploadUrl: string }>(
680
690
  `${this.baseUrl}/api/org-files/upload/${file.id}`,
681
- { fileName },
691
+ { fileName, sha256Hash },
682
692
  { headers: this.headers }
683
693
  );
684
694
  return response.data.uploadUrl;
@@ -1,6 +1,5 @@
1
1
  import * as fs from "fs";
2
2
  import { createWriteStream, createReadStream } from "fs";
3
- import * as crypto from "crypto";
4
3
  import { pipeline, Readable } from "stream";
5
4
  import * as util from "util";
6
5
 
@@ -15,19 +14,10 @@ export class S3Service {
15
14
  const fileContent = fs.readFileSync(filePath);
16
15
  const fileStats = await fs.promises.stat(filePath);
17
16
 
18
- // Compute SHA-256 checksum (base64) — required when presigned URL was
19
- // generated with ChecksumAlgorithm: SHA256
20
- const sha256Base64 = crypto
21
- .createHash("sha256")
22
- .update(fileContent)
23
- .digest("base64");
24
-
25
17
  const response = await fetch(presignedUrl, {
26
18
  method: "PUT",
27
19
  headers: {
28
20
  "Content-Length": String(fileStats.size),
29
- "x-amz-checksum-sha256": sha256Base64,
30
- "x-amz-sdk-checksum-algorithm": "SHA256",
31
21
  },
32
22
  body: fileContent,
33
23
  // @ts-ignore