@tyvm/knowhow 0.0.108 → 0.0.109-dev.05fe5a0

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 (210) 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 +50 -17
  28. package/src/index.ts +18 -0
  29. package/src/logger.ts +197 -0
  30. package/src/plugins/embedding.ts +11 -6
  31. package/src/plugins/plugins.ts +0 -21
  32. package/src/plugins/vim.ts +5 -16
  33. package/src/processors/JsonCompressor.ts +6 -6
  34. package/src/services/EventService.ts +61 -1
  35. package/src/services/KnowhowClient.ts +34 -4
  36. package/src/services/modules/index.ts +95 -51
  37. package/src/services/modules/types.ts +6 -0
  38. package/src/tunnel.ts +216 -0
  39. package/src/types.ts +0 -1
  40. package/src/worker.ts +105 -312
  41. package/src/workers/auth/WsMiddleware.ts +99 -0
  42. package/src/workers/auth/authMiddleware.ts +104 -0
  43. package/src/workers/auth/types.ts +14 -2
  44. package/src/workers/tools/index.ts +2 -0
  45. package/src/workers/tools/reloadConfig.ts +84 -0
  46. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  47. package/tests/unit/commands/github-credentials.test.ts +211 -0
  48. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  49. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  50. package/ts_build/package.json +9 -4
  51. package/ts_build/src/agents/base/base.js +11 -0
  52. package/ts_build/src/agents/base/base.js.map +1 -1
  53. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  54. package/ts_build/src/agents/tools/execCommand.js +39 -5
  55. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  56. package/ts_build/src/agents/tools/index.d.ts +0 -1
  57. package/ts_build/src/agents/tools/index.js +0 -1
  58. package/ts_build/src/agents/tools/index.js.map +1 -1
  59. package/ts_build/src/agents/tools/list.js +0 -2
  60. package/ts_build/src/agents/tools/list.js.map +1 -1
  61. package/ts_build/src/chat/CliChatService.js +13 -1
  62. package/ts_build/src/chat/CliChatService.js.map +1 -1
  63. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  64. package/ts_build/src/chat/modules/AgentModule.js +43 -20
  65. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  66. package/ts_build/src/chat/modules/SessionsModule.js +37 -3
  67. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  68. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  69. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  70. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  71. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  72. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  73. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  74. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  75. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  76. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  77. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  78. package/ts_build/src/cli.js +47 -519
  79. package/ts_build/src/cli.js.map +1 -1
  80. package/ts_build/src/clients/anthropic.d.ts +5 -5
  81. package/ts_build/src/clients/anthropic.js +17 -16
  82. package/ts_build/src/clients/anthropic.js.map +1 -1
  83. package/ts_build/src/clients/index.js +2 -4
  84. package/ts_build/src/clients/index.js.map +1 -1
  85. package/ts_build/src/clients/types.d.ts +3 -2
  86. package/ts_build/src/cloudWorker.d.ts +14 -0
  87. package/ts_build/src/cloudWorker.js +105 -66
  88. package/ts_build/src/cloudWorker.js.map +1 -1
  89. package/ts_build/src/commands/agent.d.ts +6 -0
  90. package/ts_build/src/commands/agent.js +229 -0
  91. package/ts_build/src/commands/agent.js.map +1 -0
  92. package/ts_build/src/commands/misc.d.ts +10 -0
  93. package/ts_build/src/commands/misc.js +197 -0
  94. package/ts_build/src/commands/misc.js.map +1 -0
  95. package/ts_build/src/commands/modules.d.ts +3 -0
  96. package/ts_build/src/commands/modules.js +207 -0
  97. package/ts_build/src/commands/modules.js.map +1 -0
  98. package/ts_build/src/commands/services.d.ts +5 -0
  99. package/ts_build/src/commands/services.js +87 -0
  100. package/ts_build/src/commands/services.js.map +1 -0
  101. package/ts_build/src/commands/workers.d.ts +6 -0
  102. package/ts_build/src/commands/workers.js +168 -0
  103. package/ts_build/src/commands/workers.js.map +1 -0
  104. package/ts_build/src/config.d.ts +1 -0
  105. package/ts_build/src/config.js +32 -0
  106. package/ts_build/src/config.js.map +1 -1
  107. package/ts_build/src/fileSync.d.ts +6 -0
  108. package/ts_build/src/fileSync.js +37 -12
  109. package/ts_build/src/fileSync.js.map +1 -1
  110. package/ts_build/src/index.d.ts +1 -0
  111. package/ts_build/src/index.js +17 -1
  112. package/ts_build/src/index.js.map +1 -1
  113. package/ts_build/src/logger.d.ts +21 -0
  114. package/ts_build/src/logger.js +106 -0
  115. package/ts_build/src/logger.js.map +1 -0
  116. package/ts_build/src/plugins/embedding.js +4 -3
  117. package/ts_build/src/plugins/embedding.js.map +1 -1
  118. package/ts_build/src/plugins/plugins.d.ts +0 -2
  119. package/ts_build/src/plugins/plugins.js +0 -11
  120. package/ts_build/src/plugins/plugins.js.map +1 -1
  121. package/ts_build/src/plugins/vim.js +3 -9
  122. package/ts_build/src/plugins/vim.js.map +1 -1
  123. package/ts_build/src/processors/JsonCompressor.js +4 -4
  124. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  125. package/ts_build/src/services/EventService.d.ts +6 -1
  126. package/ts_build/src/services/EventService.js +29 -0
  127. package/ts_build/src/services/EventService.js.map +1 -1
  128. package/ts_build/src/services/KnowhowClient.d.ts +13 -1
  129. package/ts_build/src/services/KnowhowClient.js +19 -2
  130. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  131. package/ts_build/src/services/modules/index.d.ts +33 -0
  132. package/ts_build/src/services/modules/index.js +67 -47
  133. package/ts_build/src/services/modules/index.js.map +1 -1
  134. package/ts_build/src/services/modules/types.d.ts +6 -0
  135. package/ts_build/src/tunnel.d.ts +27 -0
  136. package/ts_build/src/tunnel.js +112 -0
  137. package/ts_build/src/tunnel.js.map +1 -0
  138. package/ts_build/src/types.d.ts +0 -1
  139. package/ts_build/src/types.js.map +1 -1
  140. package/ts_build/src/worker.d.ts +1 -4
  141. package/ts_build/src/worker.js +59 -227
  142. package/ts_build/src/worker.js.map +1 -1
  143. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  144. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  145. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  146. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  147. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  148. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  149. package/ts_build/src/workers/auth/types.d.ts +8 -1
  150. package/ts_build/src/workers/tools/index.d.ts +2 -0
  151. package/ts_build/src/workers/tools/index.js +4 -1
  152. package/ts_build/src/workers/tools/index.js.map +1 -1
  153. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  154. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  155. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  156. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  157. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  158. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
  159. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  160. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  161. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  162. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  163. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  164. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  165. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  166. package/src/agents/tools/executeScript/README.md +0 -94
  167. package/src/agents/tools/executeScript/definition.ts +0 -79
  168. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  169. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  170. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  171. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  172. package/src/agents/tools/executeScript/index.ts +0 -98
  173. package/src/services/script-execution/SandboxContext.ts +0 -282
  174. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  175. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  176. package/src/services/script-execution/ScriptTracer.ts +0 -249
  177. package/src/services/script-execution/types.ts +0 -134
  178. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  179. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  180. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  181. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  182. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  183. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  184. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  185. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  186. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  187. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  188. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  189. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  190. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  191. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  192. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  193. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  194. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  195. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  196. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  197. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  198. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  199. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  200. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  201. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  202. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  203. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  204. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  205. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  206. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  207. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  208. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  209. package/ts_build/src/services/script-execution/types.js +0 -3
  210. package/ts_build/src/services/script-execution/types.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ import { includedTools } from "../agents/tools/list";
2
+ import * as allTools from "../agents/tools";
3
+ import { LazyToolsService, services } from "../services";
4
+ import { agents } from "../agents";
5
+ import { ModulesService } from "../services/modules";
6
+
7
+ /**
8
+ * Shared service setup used by commands that need full services (chat, agent, worker, etc.)
9
+ */
10
+ export async function setupServices() {
11
+ const {
12
+ Agents,
13
+ Mcp,
14
+ Clients,
15
+ Tools: AllTools,
16
+ Embeddings,
17
+ Plugins,
18
+ Events,
19
+ MediaProcessor,
20
+ } = services();
21
+
22
+
23
+ // cli uses LazyTools to keep context slim
24
+ const Tools = new LazyToolsService();
25
+
26
+ Tools.setContext({
27
+ ...AllTools.getContext(),
28
+ });
29
+
30
+ const agentContext: import("../agents/base/base").AgentContext = {
31
+ ...services(),
32
+ Tools,
33
+ };
34
+
35
+ const { Researcher, Developer, Patcher, Setup } = agents({
36
+ ...agentContext,
37
+ });
38
+
39
+ Agents.registerAgent(Researcher);
40
+ Agents.registerAgent(Patcher);
41
+ Agents.registerAgent(Developer);
42
+ Agents.registerAgent(Setup);
43
+ Agents.loadAgentsFromConfig(agentContext);
44
+
45
+ Tools.defineTools(includedTools, allTools);
46
+
47
+ Tools.addContext("Mcp", Mcp);
48
+
49
+ Agents.setAgentContext(agentContext);
50
+
51
+ console.log("🔌 Connecting to MCP...");
52
+ try {
53
+ await Mcp.connectToConfigured(Tools);
54
+ } catch (mcpError) {
55
+ const msg = mcpError instanceof Error ? mcpError.message : String(mcpError);
56
+ console.warn(
57
+ `⚠ Some MCP servers failed to connect (continuing without them): ${msg}`
58
+ );
59
+ }
60
+ console.log("Connecting to clients...");
61
+ await Clients.registerConfiguredModels();
62
+ console.log("✓ Services are set up and ready to go!");
63
+
64
+ console.log("📦 Loading modules from config...");
65
+ const modulesService = new ModulesService();
66
+ await modulesService.loadModulesFromConfig({
67
+ Agents,
68
+ Embeddings,
69
+ Plugins,
70
+ Clients,
71
+ Tools,
72
+ MediaProcessor,
73
+ Events
74
+ });
75
+
76
+ return { Tools, Clients };
77
+ }
@@ -0,0 +1,168 @@
1
+ import { Command } from "commander";
2
+ import { worker } from "../worker";
3
+ import { TUNNEL_MINIMAL_TOOLS } from "../tunnel";
4
+ import { fileSync } from "../fileSync";
5
+ import {
6
+ startAllWorkers,
7
+ listWorkerPaths,
8
+ unregisterWorkerPath,
9
+ clearWorkerRegistry,
10
+ } from "../workerRegistry";
11
+
12
+ export function addWorkerCommand(program: Command): void {
13
+ program
14
+ .command("worker")
15
+ .description(
16
+ "Start worker process and optionally register current directory"
17
+ )
18
+ .option("--register", "Register current directory as a worker path")
19
+ .option(
20
+ "--share",
21
+ "Share this worker with your organization (allows other users to use it)"
22
+ )
23
+ .option("--unshare", "Make this worker private (only you can use it)")
24
+ .option("--sandbox", "Run worker in a Docker container for isolation")
25
+ .option(
26
+ "--no-sandbox",
27
+ "Run worker directly on host (disable sandbox mode)"
28
+ )
29
+ .option("--passkey", "Set up passkey authentication for this worker")
30
+ .option("--passkey-reset", "Remove passkey authentication requirement")
31
+ .action(async (options) => {
32
+ const { setupServices } = await import("./services");
33
+ await setupServices();
34
+ await worker(options);
35
+ });
36
+ }
37
+
38
+ export function addWorkersCommand(program: Command): void {
39
+ program
40
+ .command("workers")
41
+ .description("Manage and start all registered workers")
42
+ .option("--list", "List all registered worker paths")
43
+ .option("--unregister <path>", "Unregister a worker path")
44
+ .option("--clear", "Clear all registered worker paths")
45
+ .action(async (options) => {
46
+ try {
47
+ if (options.list) {
48
+ const workers = await listWorkerPaths();
49
+ if (workers.length === 0) {
50
+ console.log("No workers registered.");
51
+ console.log(
52
+ "\nTo register a worker, run 'knowhow worker --register' from the worker directory."
53
+ );
54
+ } else {
55
+ console.log(`Registered workers (${workers.length}):`);
56
+ workers.forEach((workerPath, index) => {
57
+ console.log(` ${index + 1}. ${workerPath}`);
58
+ });
59
+ }
60
+ return;
61
+ }
62
+
63
+ if (options.unregister) {
64
+ await unregisterWorkerPath(options.unregister);
65
+ return;
66
+ }
67
+
68
+ if (options.clear) {
69
+ await clearWorkerRegistry();
70
+ return;
71
+ }
72
+
73
+ // Default action: start all workers
74
+ const { setupServices } = await import("./services");
75
+ await setupServices();
76
+ await startAllWorkers();
77
+ } catch (error) {
78
+ console.error("Error managing workers:", error);
79
+ process.exit(1);
80
+ }
81
+ });
82
+ }
83
+
84
+ export function addTunnelCommand(program: Command): void {
85
+ program
86
+ .command("tunnel")
87
+ .description(
88
+ "Start a minimal worker with tunnel enabled: exposes local ports to the cloud. " +
89
+ "Registers essential tools (unlock, lock, listAllowedPorts) so the backend is aware of the worker and ports. " +
90
+ "If passkey auth is configured, the tunnel is locked until unlocked via tool call or WebSocket auth protocol."
91
+ )
92
+ .option(
93
+ "--share",
94
+ "Share this tunnel with your organization (allows other users to use it)"
95
+ )
96
+ .option("--unshare", "Make this tunnel private (only you can use it)")
97
+ .action(async (options) => {
98
+ console.log("🌐 Starting tunnel (minimal worker) mode...");
99
+ console.log(` Tools: ${TUNNEL_MINIMAL_TOOLS.join(", ")}`);
100
+ await worker({
101
+ ...options,
102
+ allowedTools: TUNNEL_MINIMAL_TOOLS,
103
+ });
104
+ });
105
+ }
106
+
107
+ export function addFilesCommand(program: Command): void {
108
+ program
109
+ .command("files")
110
+ .description(
111
+ "Sync files between local filesystem and Knowhow FS (uses fileMounts config)"
112
+ )
113
+ .option("--upload", "Force upload direction for all mounts")
114
+ .option("--download", "Force download direction for all mounts")
115
+ .option("--config <path>", "Path to knowhow.json", "./knowhow.json")
116
+ .option("--dry-run", "Print what would be synced without doing it")
117
+ .action(async (options) => {
118
+ try {
119
+ await fileSync(options);
120
+ } catch (error) {
121
+ console.error("Error syncing files:", error);
122
+ process.exit(1);
123
+ }
124
+ });
125
+ }
126
+
127
+ export function addCloudWorkerCommand(program: Command): void {
128
+ program
129
+ .command("cloudworker")
130
+ .description("Create or sync a cloud worker with your local knowhow config")
131
+ .option(
132
+ "--init",
133
+ "Initialize config.files entries based on what exists in .knowhow/ (run once before --push)"
134
+ )
135
+ .option(
136
+ "--create",
137
+ "Create a new cloud worker with synced config and files"
138
+ )
139
+ .option(
140
+ "--push <uid>",
141
+ "Push/sync local config and files to an existing cloud worker"
142
+ )
143
+ .option(
144
+ "--pull <id>",
145
+ "Pull the latest workerConfigJson from a cloud worker and update local config"
146
+ )
147
+ .option("--name <name>", "Name for the cloud worker (used with --create)")
148
+ .option("--dry-run", "Print what would be synced without doing it")
149
+ .action(async (options) => {
150
+ try {
151
+ const { cloudWorker, pullCloudWorkerConfig, initCloudWorker } = await import(
152
+ "../cloudWorker"
153
+ );
154
+ if (options.init) {
155
+ await initCloudWorker({ dryRun: options.dryRun });
156
+ return;
157
+ }
158
+ if (options.pull) {
159
+ await pullCloudWorkerConfig({ id: options.pull });
160
+ } else {
161
+ await cloudWorker(options);
162
+ }
163
+ } catch (error) {
164
+ console.error("Error running cloudworker:", error);
165
+ process.exit(1);
166
+ }
167
+ });
168
+ }
package/src/config.ts CHANGED
@@ -168,6 +168,27 @@ export async function init() {
168
168
  console.log("Initializing global knowhow config at ~/.knowhow");
169
169
  const globalConfigDir = await ensureGlobalConfigDir();
170
170
 
171
+ // Ensure the script module is registered in the global config so that
172
+ // `knowhow script` and the `executeScript` tool are available everywhere.
173
+ const SCRIPT_MODULE = "@tyvm/knowhow-module-script";
174
+ const globalConfigPath = path.join(globalConfigDir, "knowhow.json");
175
+ try {
176
+ const rawGlobal = fs.existsSync(globalConfigPath)
177
+ ? fs.readFileSync(globalConfigPath, "utf8")
178
+ : JSON.stringify(defaultConfig, null, 2);
179
+ const globalConf = JSON.parse(rawGlobal) as Config;
180
+ if (!globalConf.modules) {
181
+ globalConf.modules = [];
182
+ }
183
+ if (!globalConf.modules.includes(SCRIPT_MODULE)) {
184
+ globalConf.modules.push(SCRIPT_MODULE);
185
+ fs.writeFileSync(globalConfigPath, JSON.stringify(globalConf, null, 2));
186
+ console.log(`✅ Added ${SCRIPT_MODULE} to ~/.knowhow/knowhow.json modules`);
187
+ }
188
+ } catch (e) {
189
+ console.warn(`⚠ Could not update global config to add script module:`, e);
190
+ }
191
+
171
192
  // create the folder structure
172
193
  console.log("Initializing local knowhow config at ./.knowhow");
173
194
  await mkdir(".knowhow", { recursive: true });
@@ -287,6 +308,22 @@ export async function getGlobalConfig(): Promise<Config> {
287
308
  }
288
309
  }
289
310
 
311
+ export async function updateGlobalConfig(config: Config) {
312
+ if (!config || typeof config !== "object") {
313
+ throw new Error("Invalid config object");
314
+ }
315
+
316
+ const globalConfigDir = getGlobalConfigDir();
317
+ await mkdir(globalConfigDir, { recursive: true });
318
+ const globalConfigPath = path.join(globalConfigDir, "knowhow.json");
319
+
320
+ if (fs.existsSync(globalConfigPath)) {
321
+ await fs.promises.copyFile(globalConfigPath, globalConfigPath + ".bak");
322
+ }
323
+
324
+ await writeFile(globalConfigPath, JSON.stringify(config, null, 2));
325
+ }
326
+
290
327
  export async function migrateConfig() {
291
328
  // Apply migrations, used to keep config structure up to date.
292
329
  if (!fs.existsSync(".knowhow/knowhow.json")) {
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";
@@ -138,6 +139,23 @@ export async function upload() {
138
139
  if (!source.remoteId) {
139
140
  throw new Error("remoteId is required for knowhow uploads");
140
141
  }
142
+ // Warn if the local embeddingModel differs from the one stored on the backend
143
+ try {
144
+ const remoteEmbedding = await knowhowApiClient.getOrgEmbedding(source.remoteId);
145
+ const localModel = config.embeddingModel || EmbeddingModels.openai.EmbeddingAda2;
146
+ const remoteModel = remoteEmbedding?.modelName;
147
+ if (remoteModel && remoteModel !== localModel) {
148
+ console.warn(
149
+ `⚠️ WARNING: Embedding model mismatch for "${remoteEmbedding.name}" (remoteId: ${source.remoteId}).\n` +
150
+ ` Local config.embeddingModel: ${localModel}\n` +
151
+ ` Backend embedding modelName: ${remoteModel}\n` +
152
+ ` Vectors generated with different models are not comparable — search results will be incorrect.\n` +
153
+ ` Update your config.embeddingModel to "${remoteModel}" or update the backend embedding to "${localModel}".`
154
+ );
155
+ }
156
+ } catch (e) {
157
+ // Non-fatal — don't block upload if metadata fetch fails
158
+ }
141
159
  const url = await knowhowApiClient.getPresignedUploadUrl(source);
142
160
  console.log("Uploading to", url);
143
161
  await AwsS3.uploadToPresignedUrl(url, source.output);
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
+ };