@tyvm/knowhow 0.0.108 → 0.0.109-dev.38b1faa

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 (236) 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 +2 -4
  9. package/src/chat/CliChatService.ts +11 -2
  10. package/src/chat/modules/AgentModule.ts +61 -31
  11. package/src/chat/modules/SessionsModule.ts +47 -3
  12. package/src/chat/modules/SystemModule.ts +2 -2
  13. package/src/chat/renderer/CompactRenderer.ts +20 -0
  14. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  15. package/src/chat/renderer/FancyRenderer.ts +19 -0
  16. package/src/chat/renderer/types.ts +11 -0
  17. package/src/cli.ts +91 -659
  18. package/src/clients/anthropic.ts +18 -17
  19. package/src/clients/index.ts +31 -11
  20. package/src/clients/openai.ts +8 -5
  21. package/src/clients/types.ts +48 -10
  22. package/src/clients/withRetry.ts +89 -0
  23. package/src/cloudWorker.ts +175 -113
  24. package/src/commands/agent.ts +246 -0
  25. package/src/commands/misc.ts +174 -0
  26. package/src/commands/modules.ts +552 -0
  27. package/src/commands/services.ts +77 -0
  28. package/src/commands/workers.ts +168 -0
  29. package/src/config.ts +38 -1
  30. package/src/fileSync.ts +70 -29
  31. package/src/hashes.ts +35 -13
  32. package/src/index.ts +18 -0
  33. package/src/logger.ts +197 -0
  34. package/src/plugins/embedding.ts +11 -6
  35. package/src/plugins/plugins.ts +0 -21
  36. package/src/plugins/vim.ts +5 -16
  37. package/src/processors/JsonCompressor.ts +6 -6
  38. package/src/services/EventService.ts +61 -1
  39. package/src/services/KnowhowClient.ts +34 -4
  40. package/src/services/MediaProcessorService.ts +4 -2
  41. package/src/services/modules/index.ts +102 -53
  42. package/src/services/modules/types.ts +6 -0
  43. package/src/tunnel.ts +216 -0
  44. package/src/types.ts +0 -1
  45. package/src/worker.ts +105 -312
  46. package/src/workers/auth/WsMiddleware.ts +99 -0
  47. package/src/workers/auth/authMiddleware.ts +104 -0
  48. package/src/workers/auth/types.ts +14 -2
  49. package/src/workers/tools/index.ts +2 -0
  50. package/src/workers/tools/reloadConfig.ts +84 -0
  51. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  52. package/tests/unit/clients/AIClient.test.ts +446 -0
  53. package/tests/unit/clients/withRetry.test.ts +319 -0
  54. package/tests/unit/commands/github-credentials.test.ts +210 -0
  55. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  56. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  57. package/ts_build/package.json +9 -4
  58. package/ts_build/src/agents/base/base.js +11 -0
  59. package/ts_build/src/agents/base/base.js.map +1 -1
  60. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  61. package/ts_build/src/agents/tools/execCommand.js +39 -5
  62. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  63. package/ts_build/src/agents/tools/index.d.ts +0 -1
  64. package/ts_build/src/agents/tools/index.js +0 -1
  65. package/ts_build/src/agents/tools/index.js.map +1 -1
  66. package/ts_build/src/agents/tools/list.js +2 -4
  67. package/ts_build/src/agents/tools/list.js.map +1 -1
  68. package/ts_build/src/chat/CliChatService.js +14 -2
  69. package/ts_build/src/chat/CliChatService.js.map +1 -1
  70. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  71. package/ts_build/src/chat/modules/AgentModule.js +43 -20
  72. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  73. package/ts_build/src/chat/modules/SessionsModule.js +37 -3
  74. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  75. package/ts_build/src/chat/modules/SystemModule.js +2 -2
  76. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  77. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  78. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  79. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  80. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  81. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  82. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  83. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  84. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  85. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  86. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  87. package/ts_build/src/cli.js +47 -519
  88. package/ts_build/src/cli.js.map +1 -1
  89. package/ts_build/src/clients/anthropic.d.ts +5 -5
  90. package/ts_build/src/clients/anthropic.js +18 -17
  91. package/ts_build/src/clients/anthropic.js.map +1 -1
  92. package/ts_build/src/clients/index.js +9 -10
  93. package/ts_build/src/clients/index.js.map +1 -1
  94. package/ts_build/src/clients/openai.js +4 -4
  95. package/ts_build/src/clients/openai.js.map +1 -1
  96. package/ts_build/src/clients/types.d.ts +15 -8
  97. package/ts_build/src/clients/withRetry.d.ts +2 -0
  98. package/ts_build/src/clients/withRetry.js +60 -0
  99. package/ts_build/src/clients/withRetry.js.map +1 -0
  100. package/ts_build/src/cloudWorker.d.ts +14 -0
  101. package/ts_build/src/cloudWorker.js +105 -66
  102. package/ts_build/src/cloudWorker.js.map +1 -1
  103. package/ts_build/src/commands/agent.d.ts +6 -0
  104. package/ts_build/src/commands/agent.js +229 -0
  105. package/ts_build/src/commands/agent.js.map +1 -0
  106. package/ts_build/src/commands/misc.d.ts +10 -0
  107. package/ts_build/src/commands/misc.js +197 -0
  108. package/ts_build/src/commands/misc.js.map +1 -0
  109. package/ts_build/src/commands/modules.d.ts +3 -0
  110. package/ts_build/src/commands/modules.js +487 -0
  111. package/ts_build/src/commands/modules.js.map +1 -0
  112. package/ts_build/src/commands/services.d.ts +5 -0
  113. package/ts_build/src/commands/services.js +87 -0
  114. package/ts_build/src/commands/services.js.map +1 -0
  115. package/ts_build/src/commands/workers.d.ts +6 -0
  116. package/ts_build/src/commands/workers.js +168 -0
  117. package/ts_build/src/commands/workers.js.map +1 -0
  118. package/ts_build/src/config.d.ts +1 -0
  119. package/ts_build/src/config.js +33 -1
  120. package/ts_build/src/config.js.map +1 -1
  121. package/ts_build/src/fileSync.d.ts +6 -0
  122. package/ts_build/src/fileSync.js +50 -23
  123. package/ts_build/src/fileSync.js.map +1 -1
  124. package/ts_build/src/hashes.d.ts +2 -2
  125. package/ts_build/src/hashes.js +35 -9
  126. package/ts_build/src/hashes.js.map +1 -1
  127. package/ts_build/src/index.d.ts +1 -0
  128. package/ts_build/src/index.js +17 -1
  129. package/ts_build/src/index.js.map +1 -1
  130. package/ts_build/src/logger.d.ts +21 -0
  131. package/ts_build/src/logger.js +106 -0
  132. package/ts_build/src/logger.js.map +1 -0
  133. package/ts_build/src/plugins/embedding.js +4 -3
  134. package/ts_build/src/plugins/embedding.js.map +1 -1
  135. package/ts_build/src/plugins/plugins.d.ts +0 -2
  136. package/ts_build/src/plugins/plugins.js +0 -11
  137. package/ts_build/src/plugins/plugins.js.map +1 -1
  138. package/ts_build/src/plugins/vim.js +3 -9
  139. package/ts_build/src/plugins/vim.js.map +1 -1
  140. package/ts_build/src/processors/JsonCompressor.js +4 -4
  141. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  142. package/ts_build/src/services/EventService.d.ts +6 -1
  143. package/ts_build/src/services/EventService.js +29 -0
  144. package/ts_build/src/services/EventService.js.map +1 -1
  145. package/ts_build/src/services/KnowhowClient.d.ts +13 -1
  146. package/ts_build/src/services/KnowhowClient.js +19 -2
  147. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  148. package/ts_build/src/services/MediaProcessorService.d.ts +2 -1
  149. package/ts_build/src/services/MediaProcessorService.js +3 -1
  150. package/ts_build/src/services/MediaProcessorService.js.map +1 -1
  151. package/ts_build/src/services/modules/index.d.ts +33 -0
  152. package/ts_build/src/services/modules/index.js +73 -49
  153. package/ts_build/src/services/modules/index.js.map +1 -1
  154. package/ts_build/src/services/modules/types.d.ts +6 -0
  155. package/ts_build/src/tunnel.d.ts +27 -0
  156. package/ts_build/src/tunnel.js +112 -0
  157. package/ts_build/src/tunnel.js.map +1 -0
  158. package/ts_build/src/types.d.ts +0 -1
  159. package/ts_build/src/types.js.map +1 -1
  160. package/ts_build/src/worker.d.ts +1 -4
  161. package/ts_build/src/worker.js +59 -227
  162. package/ts_build/src/worker.js.map +1 -1
  163. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  164. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  165. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  166. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  167. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  168. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  169. package/ts_build/src/workers/auth/types.d.ts +8 -1
  170. package/ts_build/src/workers/tools/index.d.ts +2 -0
  171. package/ts_build/src/workers/tools/index.js +4 -1
  172. package/ts_build/src/workers/tools/index.js.map +1 -1
  173. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  174. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  175. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  176. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  177. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  178. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
  179. package/ts_build/tests/unit/clients/AIClient.test.d.ts +1 -0
  180. package/ts_build/tests/unit/clients/AIClient.test.js +339 -0
  181. package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -0
  182. package/ts_build/tests/unit/clients/withRetry.test.d.ts +1 -0
  183. package/ts_build/tests/unit/clients/withRetry.test.js +225 -0
  184. package/ts_build/tests/unit/clients/withRetry.test.js.map +1 -0
  185. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  186. package/ts_build/tests/unit/commands/github-credentials.test.js +145 -0
  187. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  188. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  189. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  190. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  191. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  192. package/src/agents/tools/executeScript/README.md +0 -94
  193. package/src/agents/tools/executeScript/definition.ts +0 -79
  194. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  195. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  196. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  197. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  198. package/src/agents/tools/executeScript/index.ts +0 -98
  199. package/src/services/script-execution/SandboxContext.ts +0 -282
  200. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  201. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  202. package/src/services/script-execution/ScriptTracer.ts +0 -249
  203. package/src/services/script-execution/types.ts +0 -134
  204. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  205. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  206. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  207. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  208. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  209. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  210. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  211. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  212. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  213. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  214. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  215. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  216. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  217. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  218. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  219. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  220. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  221. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  222. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  223. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  224. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  225. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  226. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  227. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  228. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  229. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  230. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  231. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  232. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  233. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  234. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  235. package/ts_build/src/services/script-execution/types.js +0 -3
  236. package/ts_build/src/services/script-execution/types.js.map +0 -1
@@ -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
@@ -74,7 +74,7 @@ const defaultConfig = {
74
74
  description:
75
75
  "You can define agents in the config. They will have access to all tools.",
76
76
  instructions: "Reply to the user saying 'Hello, world!'",
77
- model: "gpt-4o-2024-08-06",
77
+ model: "gpt-5.4-nano",
78
78
  provider: "openai",
79
79
  },
80
80
  ],
@@ -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,11 +1,14 @@
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";
6
7
  import { services } from "./services";
7
8
  import { S3Service } from "./services/S3";
8
- import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
9
+ import { getHashes, saveHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
10
+
11
+ export const DEFAULT_BATCH_SIZE = 5;
9
12
 
10
13
  export interface FileSyncOptions {
11
14
  upload?: 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,16 +157,16 @@ 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,
137
167
  localPath: string,
138
- dryRun: boolean
168
+ dryRun: boolean,
169
+ hashes?: any
139
170
  ): Promise<void> {
140
171
  console.log(`⬇️ Downloading ${remotePath} → ${localPath}`);
141
172
 
@@ -146,8 +177,7 @@ async function downloadFile(
146
177
 
147
178
  try {
148
179
  // Fast-path: check stored download hash before hitting the API
149
- const hashes = await getHashes();
150
- if (await isLocalFileMatchingDownloadHash(localPath, hashes)) {
180
+ if (hashes && await isLocalFileMatchingDownloadHash(localPath, hashes)) {
151
181
  console.log(` ✓ Skipping ${localPath} (matches stored download hash)`);
152
182
  return;
153
183
  }
@@ -159,7 +189,7 @@ async function downloadFile(
159
189
  if (isLocalFileMatchingRemote(localPath, checksumSHA256)) {
160
190
  console.log(` ✓ Skipping ${localPath} (matches remote checksum)`);
161
191
  // Store the hash so future syncs can skip without hitting the API
162
- await saveDownloadHash(localPath);
192
+ await saveDownloadHash(localPath, hashes);
163
193
  return;
164
194
  }
165
195
 
@@ -173,7 +203,7 @@ async function downloadFile(
173
203
  await s3Service.downloadFromPresignedUrl(downloadUrl, localPath);
174
204
 
175
205
  // Save download hash so we can skip unchanged files next time
176
- await saveDownloadHash(localPath);
206
+ await saveDownloadHash(localPath, hashes);
177
207
 
178
208
  // Get file size for logging
179
209
  const stats = fs.statSync(localPath);
@@ -186,12 +216,13 @@ 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,
193
223
  localPath: string,
194
- dryRun: boolean
224
+ dryRun: boolean,
225
+ hashes?: any
195
226
  ): Promise<void> {
196
227
  console.log(`⬆️ Uploading ${localPath} → ${remotePath}`);
197
228
 
@@ -207,15 +238,14 @@ async function uploadFile(
207
238
  }
208
239
 
209
240
  // Skip upload if file hasn't changed since last upload
210
- const hashes = await getHashes();
211
- const changed = await hasFileChangedSinceUpload(localPath, hashes);
241
+ const changed = hashes ? await hasFileChangedSinceUpload(localPath, hashes) : true;
212
242
  if (!changed) {
213
243
  console.log(` ✓ Skipping ${localPath} (unchanged since last upload)`);
214
244
  return;
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);
@@ -224,7 +254,7 @@ async function uploadFile(
224
254
  await client.markOrgFileUploadComplete(remotePath);
225
255
 
226
256
  // Save upload hash so we can skip unchanged files next time
227
- await saveUploadHash(localPath);
257
+ await saveUploadHash(localPath, hashes);
228
258
 
229
259
  const stats = fs.statSync(localPath);
230
260
  console.log(` ✓ Uploaded ${stats.size} bytes`);
@@ -246,6 +276,8 @@ export async function uploadDirectory(
246
276
 
247
277
  console.log(`⬆️ Uploading directory ${localDir} → ${remoteDir}`);
248
278
 
279
+ const hashes = await getHashes();
280
+
249
281
  if (!fs.existsSync(localDir)) {
250
282
  console.warn(` ⚠️ Local directory not found: ${localDir}`);
251
283
  return 0;
@@ -261,26 +293,30 @@ export async function uploadDirectory(
261
293
 
262
294
  console.log(` Found ${localFiles.length} local file(s)`);
263
295
 
264
- let count = 0;
265
- for (const relFile of localFiles) {
296
+ const tasks = localFiles.map((relFile) => async () => {
266
297
  const localFilePath = localDir + relFile;
267
298
  const remoteFilePath = remoteDir + relFile;
268
299
  try {
269
- await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun);
270
- count++;
300
+ await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun, hashes);
301
+ return 1;
271
302
  } catch (error) {
272
303
  console.error(
273
304
  ` ❌ Failed to upload ${localFilePath}, skipping: ${error.message}`
274
305
  );
306
+ return 0;
275
307
  }
276
- }
277
- return count;
308
+ });
309
+
310
+ const counts = await batchRun(tasks);
311
+ await saveHashes(hashes);
312
+
313
+ return counts.reduce((sum, n) => sum + n, 0);
278
314
  }
279
315
 
280
316
  /**
281
317
  * Download all files from a remote directory path to a local directory
282
318
  */
283
- async function downloadDirectory(
319
+ export async function downloadDirectory(
284
320
  client: KnowhowSimpleClient,
285
321
  s3Service: S3Service,
286
322
  remotePath: string,
@@ -293,6 +329,8 @@ async function downloadDirectory(
293
329
 
294
330
  console.log(`⬇️ Downloading directory ${remoteDir} → ${localDir}`);
295
331
 
332
+ const hashes = await getHashes();
333
+
296
334
  // List all org files and find those in the remote directory
297
335
  const response = await client.listOrgFiles();
298
336
  const allFiles = response.data;
@@ -313,16 +351,19 @@ async function downloadDirectory(
313
351
 
314
352
  console.log(` Found ${matchingFiles.length} remote file(s)`);
315
353
 
316
- let count = 0;
317
- for (const f of matchingFiles) {
354
+ const tasks = matchingFiles.map((f) => async () => {
318
355
  const fullRemotePath = f.folderPath.endsWith("/")
319
356
  ? f.folderPath + f.fileName
320
357
  : f.folderPath + "/" + f.fileName;
321
358
  // Strip the base remote dir prefix to get relative path
322
359
  const relativePath = fullRemotePath.slice(remoteDir.length);
323
360
  const localFilePath = localDir + relativePath;
324
- await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun);
325
- count++;
326
- }
327
- return count;
361
+ await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun, hashes);
362
+ return 1;
363
+ });
364
+
365
+ const counts = await batchRun(tasks);
366
+ await saveHashes(hashes);
367
+
368
+ return counts.reduce((sum, n) => sum + n, 0);
328
369
  }
package/src/hashes.ts CHANGED
@@ -1,16 +1,35 @@
1
1
  import fs from "fs";
2
2
  import * as crypto from "crypto";
3
3
  import { Hashes } from "./types";
4
- import { readFile, writeFile } from "./utils";
4
+ import { readFile } from "./utils";
5
5
  import { convertToText } from "./conversion";
6
6
 
7
7
  export async function getHashes() {
8
- const hashes = JSON.parse(await readFile(".knowhow/.hashes.json", "utf8"));
9
- return hashes as Hashes;
8
+ try {
9
+ const hashes = JSON.parse(await readFile(".knowhow/.hashes.json", "utf8"));
10
+ return hashes as Hashes;
11
+ } catch (err: any) {
12
+ if (err.code === "ENOENT") {
13
+ return {} as Hashes;
14
+ }
15
+ throw err;
16
+ }
10
17
  }
11
18
 
19
+ /**
20
+ * Atomically save hashes to disk — writes to a temp file then renames,
21
+ * preventing concurrent writes from producing corrupted/truncated JSON.
22
+ */
12
23
  export async function saveHashes(hashes: any) {
13
- await writeFile(".knowhow/.hashes.json", JSON.stringify(hashes, null, 2));
24
+ const target = ".knowhow/.hashes.json";
25
+ const tmp = `${target}.tmp.${process.pid}`;
26
+ try {
27
+ fs.writeFileSync(tmp, JSON.stringify(hashes, null, 2));
28
+ fs.renameSync(tmp, target);
29
+ } catch (err) {
30
+ try { fs.unlinkSync(tmp); } catch (_) {}
31
+ throw err;
32
+ }
14
33
  }
15
34
 
16
35
  export async function md5Hash(str: string) {
@@ -90,17 +109,19 @@ export async function hasFileChangedSinceUpload(
90
109
  }
91
110
 
92
111
  /**
93
- * Saves the hash of the file at the time of a successful upload
112
+ * Mutates the provided hashes object with the upload hash for localPath.
113
+ * If no hashes object is provided, loads, mutates, and saves independently.
94
114
  */
95
- export async function saveUploadHash(localPath: string) {
96
- const hashes = await getHashes();
115
+ export async function saveUploadHash(localPath: string, hashes?: any) {
116
+ const standalone = !hashes;
117
+ if (standalone) hashes = await getHashes();
97
118
  const content = fs.readFileSync(localPath);
98
119
  const currentHash = crypto.createHash("md5").update(content).digest("hex");
99
120
  if (!hashes[localPath]) {
100
121
  hashes[localPath] = { fileHash: currentHash, promptHash: "" };
101
122
  }
102
123
  hashes[localPath][UPLOAD_KEY] = currentHash;
103
- await saveHashes(hashes);
124
+ if (standalone) await saveHashes(hashes);
104
125
  }
105
126
 
106
127
  /**
@@ -120,18 +141,19 @@ export async function isLocalFileMatchingDownloadHash(
120
141
  }
121
142
 
122
143
  /**
123
- * Saves the SHA-256 hash of the file after a successful download so we can
124
- * skip unchanged files on the next sync.
144
+ * Mutates the provided hashes object with the download hash for localPath.
145
+ * If no hashes object is provided, loads, mutates, and saves independently.
125
146
  */
126
- export async function saveDownloadHash(localPath: string) {
127
- const hashes = await getHashes();
147
+ export async function saveDownloadHash(localPath: string, hashes?: any) {
148
+ const standalone = !hashes;
149
+ if (standalone) hashes = await getHashes();
128
150
  const content = fs.readFileSync(localPath);
129
151
  const currentHash = crypto.createHash("sha256").update(content).digest("base64");
130
152
  if (!hashes[localPath]) {
131
153
  hashes[localPath] = { fileHash: currentHash, promptHash: "" };
132
154
  }
133
155
  hashes[localPath][DOWNLOAD_KEY] = currentHash;
134
- await saveHashes(hashes);
156
+ if (standalone) await saveHashes(hashes);
135
157
  }
136
158
 
137
159
  /**
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);