@tyvm/knowhow 0.0.108 → 0.0.109

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 (209) hide show
  1. package/README.md +45 -0
  2. package/package.json +9 -4
  3. package/scripts/publish.sh +86 -0
  4. package/src/agents/base/base.ts +10 -0
  5. package/src/agents/tools/execCommand.ts +49 -6
  6. package/src/agents/tools/index.ts +0 -1
  7. package/src/agents/tools/list.ts +0 -2
  8. package/src/chat/CliChatService.ts +10 -1
  9. package/src/chat/modules/AgentModule.ts +55 -30
  10. package/src/chat/modules/SessionsModule.ts +7 -2
  11. package/src/chat/renderer/CompactRenderer.ts +20 -0
  12. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  13. package/src/chat/renderer/FancyRenderer.ts +19 -0
  14. package/src/chat/renderer/types.ts +11 -0
  15. package/src/cli.ts +91 -659
  16. package/src/clients/anthropic.ts +17 -16
  17. package/src/clients/index.ts +6 -5
  18. package/src/clients/types.ts +19 -4
  19. package/src/cloudWorker.ts +175 -113
  20. package/src/commands/agent.ts +246 -0
  21. package/src/commands/misc.ts +174 -0
  22. package/src/commands/modules.ts +182 -0
  23. package/src/commands/services.ts +77 -0
  24. package/src/commands/workers.ts +168 -0
  25. package/src/config.ts +37 -0
  26. package/src/fileSync.ts +50 -17
  27. package/src/index.ts +18 -0
  28. package/src/logger.ts +197 -0
  29. package/src/plugins/embedding.ts +11 -6
  30. package/src/plugins/plugins.ts +0 -21
  31. package/src/plugins/vim.ts +5 -16
  32. package/src/processors/JsonCompressor.ts +6 -6
  33. package/src/services/EventService.ts +61 -1
  34. package/src/services/KnowhowClient.ts +34 -4
  35. package/src/services/modules/index.ts +70 -50
  36. package/src/services/modules/types.ts +6 -0
  37. package/src/tunnel.ts +216 -0
  38. package/src/types.ts +0 -1
  39. package/src/worker.ts +105 -312
  40. package/src/workers/auth/WsMiddleware.ts +99 -0
  41. package/src/workers/auth/authMiddleware.ts +104 -0
  42. package/src/workers/auth/types.ts +14 -2
  43. package/src/workers/tools/index.ts +2 -0
  44. package/src/workers/tools/reloadConfig.ts +84 -0
  45. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  46. package/tests/unit/commands/github-credentials.test.ts +211 -0
  47. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  48. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  49. package/ts_build/package.json +9 -4
  50. package/ts_build/src/agents/base/base.js +11 -0
  51. package/ts_build/src/agents/base/base.js.map +1 -1
  52. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  53. package/ts_build/src/agents/tools/execCommand.js +39 -5
  54. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  55. package/ts_build/src/agents/tools/index.d.ts +0 -1
  56. package/ts_build/src/agents/tools/index.js +0 -1
  57. package/ts_build/src/agents/tools/index.js.map +1 -1
  58. package/ts_build/src/agents/tools/list.js +0 -2
  59. package/ts_build/src/agents/tools/list.js.map +1 -1
  60. package/ts_build/src/chat/CliChatService.js +13 -1
  61. package/ts_build/src/chat/CliChatService.js.map +1 -1
  62. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  63. package/ts_build/src/chat/modules/AgentModule.js +39 -19
  64. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  65. package/ts_build/src/chat/modules/SessionsModule.js +7 -2
  66. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  67. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  68. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  69. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  70. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  71. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  72. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  73. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  74. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  75. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  76. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  77. package/ts_build/src/cli.js +47 -519
  78. package/ts_build/src/cli.js.map +1 -1
  79. package/ts_build/src/clients/anthropic.d.ts +5 -5
  80. package/ts_build/src/clients/anthropic.js +17 -16
  81. package/ts_build/src/clients/anthropic.js.map +1 -1
  82. package/ts_build/src/clients/index.js +2 -4
  83. package/ts_build/src/clients/index.js.map +1 -1
  84. package/ts_build/src/clients/types.d.ts +3 -2
  85. package/ts_build/src/cloudWorker.d.ts +14 -0
  86. package/ts_build/src/cloudWorker.js +105 -66
  87. package/ts_build/src/cloudWorker.js.map +1 -1
  88. package/ts_build/src/commands/agent.d.ts +6 -0
  89. package/ts_build/src/commands/agent.js +229 -0
  90. package/ts_build/src/commands/agent.js.map +1 -0
  91. package/ts_build/src/commands/misc.d.ts +10 -0
  92. package/ts_build/src/commands/misc.js +197 -0
  93. package/ts_build/src/commands/misc.js.map +1 -0
  94. package/ts_build/src/commands/modules.d.ts +3 -0
  95. package/ts_build/src/commands/modules.js +160 -0
  96. package/ts_build/src/commands/modules.js.map +1 -0
  97. package/ts_build/src/commands/services.d.ts +5 -0
  98. package/ts_build/src/commands/services.js +87 -0
  99. package/ts_build/src/commands/services.js.map +1 -0
  100. package/ts_build/src/commands/workers.d.ts +6 -0
  101. package/ts_build/src/commands/workers.js +168 -0
  102. package/ts_build/src/commands/workers.js.map +1 -0
  103. package/ts_build/src/config.d.ts +1 -0
  104. package/ts_build/src/config.js +32 -0
  105. package/ts_build/src/config.js.map +1 -1
  106. package/ts_build/src/fileSync.d.ts +6 -0
  107. package/ts_build/src/fileSync.js +37 -12
  108. package/ts_build/src/fileSync.js.map +1 -1
  109. package/ts_build/src/index.d.ts +1 -0
  110. package/ts_build/src/index.js +17 -1
  111. package/ts_build/src/index.js.map +1 -1
  112. package/ts_build/src/logger.d.ts +21 -0
  113. package/ts_build/src/logger.js +106 -0
  114. package/ts_build/src/logger.js.map +1 -0
  115. package/ts_build/src/plugins/embedding.js +4 -3
  116. package/ts_build/src/plugins/embedding.js.map +1 -1
  117. package/ts_build/src/plugins/plugins.d.ts +0 -2
  118. package/ts_build/src/plugins/plugins.js +0 -11
  119. package/ts_build/src/plugins/plugins.js.map +1 -1
  120. package/ts_build/src/plugins/vim.js +3 -9
  121. package/ts_build/src/plugins/vim.js.map +1 -1
  122. package/ts_build/src/processors/JsonCompressor.js +4 -4
  123. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  124. package/ts_build/src/services/EventService.d.ts +6 -1
  125. package/ts_build/src/services/EventService.js +29 -0
  126. package/ts_build/src/services/EventService.js.map +1 -1
  127. package/ts_build/src/services/KnowhowClient.d.ts +13 -1
  128. package/ts_build/src/services/KnowhowClient.js +19 -2
  129. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  130. package/ts_build/src/services/modules/index.d.ts +33 -0
  131. package/ts_build/src/services/modules/index.js +46 -45
  132. package/ts_build/src/services/modules/index.js.map +1 -1
  133. package/ts_build/src/services/modules/types.d.ts +6 -0
  134. package/ts_build/src/tunnel.d.ts +27 -0
  135. package/ts_build/src/tunnel.js +112 -0
  136. package/ts_build/src/tunnel.js.map +1 -0
  137. package/ts_build/src/types.d.ts +0 -1
  138. package/ts_build/src/types.js.map +1 -1
  139. package/ts_build/src/worker.d.ts +1 -4
  140. package/ts_build/src/worker.js +59 -227
  141. package/ts_build/src/worker.js.map +1 -1
  142. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  143. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  144. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  145. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  146. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  147. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  148. package/ts_build/src/workers/auth/types.d.ts +8 -1
  149. package/ts_build/src/workers/tools/index.d.ts +2 -0
  150. package/ts_build/src/workers/tools/index.js +4 -1
  151. package/ts_build/src/workers/tools/index.js.map +1 -1
  152. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  153. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  154. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  155. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  156. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  157. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
  158. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  159. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  160. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  161. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  162. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  163. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  164. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  165. package/src/agents/tools/executeScript/README.md +0 -94
  166. package/src/agents/tools/executeScript/definition.ts +0 -79
  167. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  168. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  169. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  170. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  171. package/src/agents/tools/executeScript/index.ts +0 -98
  172. package/src/services/script-execution/SandboxContext.ts +0 -282
  173. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  174. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  175. package/src/services/script-execution/ScriptTracer.ts +0 -249
  176. package/src/services/script-execution/types.ts +0 -134
  177. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  178. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  179. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  180. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  181. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  182. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  183. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  184. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  185. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  186. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  187. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  188. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  189. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  190. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  191. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  192. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  193. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  194. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  195. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  196. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  197. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  198. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  199. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  200. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  201. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  202. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  203. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  204. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  205. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  206. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  207. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  208. package/ts_build/src/services/script-execution/types.js +0 -3
  209. package/ts_build/src/services/script-execution/types.js.map +0 -1
@@ -40,11 +40,11 @@ export class GenericAnthropicClient implements GenericClient {
40
40
  });
41
41
  }
42
42
 
43
- handleToolCaching(tools: Anthropic.Tool[]) {
43
+ handleToolCaching(tools: Anthropic.Tool[], longTtl = false) {
44
44
  const lastTool = tools[tools.length - 1];
45
45
 
46
46
  if (lastTool) {
47
- lastTool.cache_control = { type: "ephemeral" };
47
+ lastTool.cache_control = longTtl ? { type: "ephemeral", ttl: "1h" } as any : { type: "ephemeral" };
48
48
  }
49
49
  }
50
50
 
@@ -94,7 +94,7 @@ export class GenericAnthropicClient implements GenericClient {
94
94
  return cleaned;
95
95
  }
96
96
 
97
- transformTools(tools?: Tool[]): Anthropic.Tool[] {
97
+ transformTools(tools?: Tool[], longTtl = false): Anthropic.Tool[] {
98
98
  if (!tools) {
99
99
  return [];
100
100
  }
@@ -104,7 +104,7 @@ export class GenericAnthropicClient implements GenericClient {
104
104
  input_schema: this.cleanSchemaForAnthropic(tool.function.parameters) as any,
105
105
  }));
106
106
 
107
- this.handleToolCaching(transformed);
107
+ this.handleToolCaching(transformed, longTtl);
108
108
 
109
109
  return transformed;
110
110
  }
@@ -153,16 +153,16 @@ export class GenericAnthropicClient implements GenericClient {
153
153
  return messages;
154
154
  }
155
155
 
156
- cacheLastContent(message: MessageParam) {
156
+ cacheLastContent(message: MessageParam, longTtl = false) {
157
157
  if (Array.isArray(message.content)) {
158
158
  const lastMessage = message.content[message.content.length - 1];
159
159
  if (
160
160
  lastMessage.type !== "thinking" &&
161
161
  lastMessage.type !== "redacted_thinking"
162
162
  ) {
163
- lastMessage.cache_control = {
164
- type: "ephemeral",
165
- };
163
+ lastMessage.cache_control = longTtl
164
+ ? ({ type: "ephemeral", ttl: "1h" } as any)
165
+ : { type: "ephemeral" };
166
166
  }
167
167
  }
168
168
  }
@@ -179,7 +179,7 @@ export class GenericAnthropicClient implements GenericClient {
179
179
  }
180
180
  }
181
181
 
182
- handleMessageCaching(groupedMessages: MessageParam[]) {
182
+ handleMessageCaching(groupedMessages: MessageParam[], longTtl = false) {
183
183
  this.handleClearingCache(groupedMessages);
184
184
 
185
185
  // find the last two messages and mark them as ephemeral
@@ -189,7 +189,7 @@ export class GenericAnthropicClient implements GenericClient {
189
189
 
190
190
  for (const m of lastTwoUserMessages) {
191
191
  if (Array.isArray(m.content)) {
192
- this.cacheLastContent(m);
192
+ this.cacheLastContent(m, longTtl);
193
193
  }
194
194
  }
195
195
  }
@@ -203,7 +203,7 @@ export class GenericAnthropicClient implements GenericClient {
203
203
  }
204
204
  }
205
205
 
206
- transformMessages(messages: Message[]): MessageParam[] {
206
+ transformMessages(messages: Message[], longTtl = false): MessageParam[] {
207
207
  const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
208
208
  const claudeMessages: MessageParam[] = messages
209
209
  .filter((msg) => msg.role !== "system")
@@ -302,7 +302,7 @@ export class GenericAnthropicClient implements GenericClient {
302
302
 
303
303
  const groupedMessages = this.combineMessages(claudeMessages);
304
304
 
305
- this.handleMessageCaching(groupedMessages);
305
+ this.handleMessageCaching(groupedMessages, longTtl);
306
306
 
307
307
  return groupedMessages;
308
308
  }
@@ -349,14 +349,15 @@ export class GenericAnthropicClient implements GenericClient {
349
349
  async createChatCompletion(
350
350
  options: CompletionOptions
351
351
  ): Promise<CompletionResponse> {
352
+ const longTtl = !!options.long_ttl_cache;
352
353
  const systemMessage = options.messages
353
354
  .filter((msg) => msg.role === "system")
354
355
  .map((msg) => msg.content || "")
355
356
  .join("\n");
356
357
 
357
- const claudeMessages = this.transformMessages(options.messages);
358
+ const claudeMessages = this.transformMessages(options.messages, longTtl);
358
359
 
359
- const tools = this.transformTools(options.tools);
360
+ const tools = this.transformTools(options.tools, longTtl);
360
361
  try {
361
362
  const response = await this.client.messages.create({
362
363
  model: options.model,
@@ -365,7 +366,7 @@ export class GenericAnthropicClient implements GenericClient {
365
366
  ? [
366
367
  {
367
368
  text: systemMessage,
368
- cache_control: { type: "ephemeral" },
369
+ cache_control: longTtl ? ({ type: "ephemeral", ttl: "1h" } as any) : { type: "ephemeral" },
369
370
  type: "text",
370
371
  },
371
372
  ]
@@ -424,7 +425,7 @@ export class GenericAnthropicClient implements GenericClient {
424
425
  usd_cost: this.calculateCost(options.model, response.usage),
425
426
  };
426
427
  } catch (err) {
427
- if ("headers" in err && err.headers["x-should-retry"] === "true") {
428
+ if ("headers" in err && err.headers?.["x-should-retry"] === "true") {
428
429
  console.warn("Retrying failed request", err);
429
430
  await wait(2500);
430
431
  return this.createChatCompletion(options);
@@ -577,11 +577,12 @@ export class AIClient {
577
577
  * @param modelQuery - the model name to search for (can be partial/normalized)
578
578
  * @param provider - optional provider to restrict search to
579
579
  */
580
- findModelFuzzy(modelQuery: string, provider?: string): { provider: string; model: string } | undefined {
580
+ findModelFuzzy(
581
+ modelQuery: string,
582
+ provider?: string
583
+ ): { provider: string; model: string } | undefined {
581
584
  const queryNorm = AIClient.normalizeModelId(modelQuery);
582
- const providers = provider
583
- ? [provider]
584
- : Object.keys(this.clientModels);
585
+ const providers = provider ? [provider] : Object.keys(this.clientModels);
585
586
 
586
587
  for (const p of providers) {
587
588
  const models = (this.clientModels[p] as string[]) ?? [];
@@ -835,7 +836,7 @@ export class AIClient {
835
836
  const splitModel = m.id.split("/");
836
837
 
837
838
  if (splitModel.length < 2) {
838
- console.error(`Cannot parse model format: ${m.id}`);
839
+ console.warn(`Cannot parse model format: ${m.id}`);
839
840
  }
840
841
 
841
842
  const provider = splitModel.length > 1 ? splitModel[0] : "";
@@ -1,4 +1,10 @@
1
- export type ModelModality = "completion" | "embedding" | "image" | "audio" | "video" | "transcription";
1
+ export type ModelModality =
2
+ | "completion"
3
+ | "embedding"
4
+ | "image"
5
+ | "audio"
6
+ | "video"
7
+ | "transcription";
2
8
 
3
9
  export type MessageContent =
4
10
  | { type: "text"; text: string }
@@ -8,7 +14,7 @@ export type MessageContent =
8
14
 
9
15
  export interface Message {
10
16
  role: "system" | "user" | "assistant" | "tool";
11
- content?: string | MessageContent[];
17
+ content?: string | MessageContent[] | null;
12
18
 
13
19
  name?: string;
14
20
  tool_call_id?: string;
@@ -16,7 +22,7 @@ export interface Message {
16
22
  }
17
23
 
18
24
  export interface OutputMessage extends Message {
19
- content: string;
25
+ content?: string | null;
20
26
  }
21
27
 
22
28
  export interface ToolProp {
@@ -61,6 +67,13 @@ export interface CompletionOptions {
61
67
  * Maps to: OpenAI reasoning_effort, xAI reasoning.effort, Gemini thinkingLevel/thinkingBudget, Anthropic thinking budget.
62
68
  * "low" = minimal thinking, "medium" = balanced, "high" = maximum reasoning */
63
69
  reasoning_effort?: "low" | "medium" | "high";
70
+ /**
71
+ * When true, hints to the client that this task is long-running and it should
72
+ * use a long-TTL cache where available.
73
+ * - Anthropic: enables the `extended-cache-ttl-2025-02-19` beta and sets
74
+ * `cache_control.ttl` to 3600 (1 hour) instead of the default 5-minute ephemeral cache.
75
+ */
76
+ long_ttl_cache?: boolean;
64
77
  }
65
78
 
66
79
  /**
@@ -294,7 +307,9 @@ export interface GenericClient {
294
307
  * When modality is provided, return only models for that modality (static list).
295
308
  * When omitted, return ALL models (backward compat — may do a live API call).
296
309
  */
297
- getModels(modality?: ModelModality): Promise<{ id: string; modality?: ModelModality[] }[]>;
310
+ getModels(
311
+ modality?: ModelModality
312
+ ): Promise<{ id: string; modality?: ModelModality[] }[]>;
298
313
  /**
299
314
  * Returns the context window limit and compression threshold for a given model,
300
315
  * or undefined if the model is not known to this client.
@@ -4,12 +4,18 @@ import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
4
4
  import { loadJwt } from "./login";
5
5
  import { getConfig, updateConfig, getLanguageConfig } from "./config";
6
6
  import { services } from "./services";
7
- import { Language, Config } from "./types";
8
- import { S3Service } from "./services/S3";
7
+ import { Language, Config, McpConfig } from "./types";
8
+ import { uploadFile, uploadDirectory } from "./fileSync";
9
+
10
+ export interface CloudWorkerPullOptions {
11
+ id: string;
12
+ apiUrl?: string;
13
+ }
9
14
 
10
15
  export interface CloudWorkerOptions {
11
16
  create?: boolean;
12
17
  push?: string; // uid of existing cloud worker
18
+ init?: boolean; // initialize config.files entries (mutates config)
13
19
  name?: string; // optional name for create
14
20
  apiUrl?: string;
15
21
  dryRun?: boolean;
@@ -25,25 +31,6 @@ interface FileToSync {
25
31
  isDirectory?: boolean; // true if this represents a whole directory
26
32
  }
27
33
 
28
- /**
29
- * Recursively list all files in a local directory, returning relative paths
30
- */
31
- function listFilesRecursively(dir: string): string[] {
32
- const results: string[] = [];
33
- if (!fs.existsSync(dir)) return results;
34
- const entries = fs.readdirSync(dir, { withFileTypes: true });
35
- for (const entry of entries) {
36
- if (entry.isDirectory()) {
37
- listFilesRecursively(path.join(dir, entry.name)).forEach((f) =>
38
- results.push(entry.name + "/" + f)
39
- );
40
- } else {
41
- results.push(entry.name);
42
- }
43
- }
44
- return results;
45
- }
46
-
47
34
  /**
48
35
  * Build the worker config JSON from the local knowhow config
49
36
  */
@@ -66,21 +53,19 @@ function buildWorkerConfigJson(config: Config, files: { remotePath: string; loca
66
53
  }
67
54
 
68
55
  /**
69
- * Collect all files from the .knowhow directory that should be synced
70
- * Uses directory-level entries where possible so the worker config stays compact
71
- * and the folder upload/download feature handles individual files automatically.
56
+ * Collect all files from the .knowhow directory that should be synced.
57
+ * Only includes files/directories that currently exist locally.
58
+ * Used by --init to populate config.files.
72
59
  */
73
60
  async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
74
61
  const filesToSync: FileToSync[] = [];
75
62
 
76
- // Helper to add file if it exists
77
63
  const addIfExists = (localPath: string, remotePath: string) => {
78
64
  if (fs.existsSync(localPath)) {
79
65
  filesToSync.push({ localPath, remotePath });
80
66
  }
81
67
  };
82
68
 
83
- // Helper to add a directory entry if it exists (trailing slash = directory mode)
84
69
  const addDirIfExists = (localPath: string, remotePath: string) => {
85
70
  if (fs.existsSync(localPath)) {
86
71
  filesToSync.push({ localPath: localPath + "/", remotePath: remotePath + "/", isDirectory: true });
@@ -103,7 +88,9 @@ async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
103
88
  }
104
89
 
105
90
  /**
106
- * Collect files referenced in language.json sources
91
+ * Collect files referenced in language.json sources.
92
+ * These are always re-collected on both --init and --push so that new
93
+ * language term sources are picked up automatically.
107
94
  */
108
95
  async function collectLanguageReferencedFiles(
109
96
  language: Language,
@@ -119,17 +106,14 @@ async function collectLanguageReferencedFiles(
119
106
  if (source.kind !== "file" || !source.data) continue;
120
107
 
121
108
  for (const filePath of source.data) {
122
- // Normalize the path (strip leading ./)
123
109
  const normalizedPath = filePath.replace(/^\.\//, "");
124
110
 
125
111
  // Skip the main knowhow config — it should not be synced to the language folder
126
- // as it would overwrite the worker's own config
127
112
  if (normalizedPath === ".knowhow/knowhow.json") continue;
128
113
 
129
114
  if (fs.existsSync(normalizedPath)) {
130
115
  const basename = path.basename(normalizedPath);
131
116
  const remotePath = `${projectName}/.knowhow/language/${basename}`;
132
- // localPath is the original path so the worker downloads it to the right place
133
117
  filesToSync.push({ localPath: normalizedPath, remotePath, downloadLocalPath: normalizedPath });
134
118
  }
135
119
  }
@@ -140,37 +124,83 @@ async function collectLanguageReferencedFiles(
140
124
  }
141
125
 
142
126
  /**
143
- * Upload a single file to the cloud worker's file storage
127
+ * Collect language-referenced files if language.json is present in the
128
+ * given config.files entries. Returns empty array if language.json is not
129
+ * configured for sync.
130
+ */
131
+ async function collectLanguageFilesIfConfigured(
132
+ configFiles: { remotePath: string; localPath: string }[],
133
+ projectName: string
134
+ ): Promise<FileToSync[]> {
135
+ const syncingLanguage = configFiles.some(
136
+ (f) => !f.remotePath.endsWith("/") && f.remotePath.endsWith("language.json")
137
+ );
138
+ if (!syncingLanguage) return [];
139
+
140
+ const language = await getLanguageConfig();
141
+ return collectLanguageReferencedFiles(language, projectName);
142
+ }
143
+
144
+ /**
145
+ * Initialize the local config.files entries based on what exists in .knowhow/.
146
+ * This is the --init step — mutates config. Run once to set up sync entries.
147
+ * language-referenced files are also collected if language.json is present.
144
148
  */
145
- async function uploadSingleFile(
146
- client: KnowhowSimpleClient,
147
- s3Service: S3Service,
148
- localPath: string,
149
- remotePath: string,
150
- dryRun: boolean
151
- ): Promise<void> {
152
- console.log(` ā¬†ļø Uploading ${localPath} → ${remotePath}`);
153
-
154
- if (dryRun) {
155
- console.log(` [DRY RUN] Would upload from ${localPath}`);
156
- return;
149
+ export async function initCloudWorker(options: { apiUrl?: string; dryRun?: boolean } = {}) {
150
+ const { dryRun = false } = options;
151
+
152
+ const config = await getConfig();
153
+ if (!config || Object.keys(config).length === 0) {
154
+ console.error("āŒ No knowhow config found. Please run 'knowhow init' first.");
155
+ process.exit(1);
157
156
  }
158
157
 
159
- if (!fs.existsSync(localPath)) {
160
- console.warn(` āš ļø Local file not found, skipping: ${localPath}`);
161
- return;
158
+ const projectName = path.basename(process.cwd());
159
+ console.log(`šŸ“ Project name: ${projectName}`);
160
+
161
+ console.log("\nšŸ“‚ Collecting files to sync...");
162
+ const mainFiles = await collectFilesToSync(projectName);
163
+ const languageFiles = await collectLanguageFilesIfConfigured(mainFiles, projectName);
164
+
165
+ if (languageFiles.length === 0 && !mainFiles.some((f) => f.remotePath.endsWith("language.json"))) {
166
+ console.log(" ā„¹ļø Skipping language-referenced files (language.json not found locally)");
162
167
  }
163
168
 
164
- const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
165
- await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
166
- await client.markOrgFileUploadComplete(remotePath);
169
+ // Deduplicate by remotePath
170
+ const allFilesMap = new Map<string, FileToSync>();
171
+ for (const f of [...mainFiles, ...languageFiles]) {
172
+ allFilesMap.set(f.remotePath, f);
173
+ }
174
+ const allFiles = Array.from(allFilesMap.values());
175
+
176
+ console.log(` Found ${allFiles.length} files to register`);
167
177
 
168
- const stats = fs.statSync(localPath);
169
- console.log(` āœ“ Uploaded ${stats.size} bytes`);
178
+ const configFilesEntries = allFiles.map((f) => ({
179
+ remotePath: f.remotePath,
180
+ localPath: f.downloadLocalPath ?? f.localPath,
181
+ direction: "download" as const,
182
+ }));
183
+
184
+ console.log("\nšŸ’¾ Updating config.files with sync entries...");
185
+ if (!dryRun) {
186
+ const existingFiles = config.files || [];
187
+ const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
188
+ const preserved = existingFiles.filter((e) => !newRemotePaths.has(e.remotePath));
189
+ config.files = [...preserved, ...configFilesEntries];
190
+ await updateConfig(config);
191
+ console.log(` āœ“ Updated config with ${config.files.length} file entries`);
192
+ } else {
193
+ console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
194
+ for (const f of allFiles) {
195
+ console.log(` ${f.localPath} → ${f.remotePath}`);
196
+ }
197
+ }
170
198
  }
171
199
 
172
200
  /**
173
- * Main cloudWorker command handler
201
+ * Main cloudWorker command handler — push/create only.
202
+ * Reads config.files (set up by --init) and also re-collects any language-referenced
203
+ * files so new language term sources are always included without requiring --init again.
174
204
  */
175
205
  export async function cloudWorker(options: CloudWorkerOptions) {
176
206
  const {
@@ -200,10 +230,6 @@ export async function cloudWorker(options: CloudWorkerOptions) {
200
230
  process.exit(1);
201
231
  }
202
232
 
203
- // Load language config
204
- const language = await getLanguageConfig();
205
-
206
- // Get project name from current directory
207
233
  const projectName = path.basename(process.cwd());
208
234
  console.log(`šŸ“ Project name: ${projectName}`);
209
235
 
@@ -213,86 +239,63 @@ export async function cloudWorker(options: CloudWorkerOptions) {
213
239
  // Get S3 service
214
240
  const { AwsS3 } = services();
215
241
 
216
- // Step 1: Collect all files to sync
217
- console.log("\nšŸ“‚ Collecting files to sync...");
218
- const mainFiles = await collectFilesToSync(projectName);
219
- const languageFiles = await collectLanguageReferencedFiles(language, projectName);
220
-
221
- // Deduplicate by remotePath
222
- const allFilesMap = new Map<string, FileToSync>();
223
- for (const f of [...mainFiles, ...languageFiles]) {
224
- allFilesMap.set(f.remotePath, f);
242
+ // Start with config.files (set up via --init)
243
+ const configFiles = config.files || [];
244
+ if (configFiles.length === 0) {
245
+ console.warn("āš ļø No files configured. Run 'knowhow cloudworker --init' first to set up file sync entries.");
225
246
  }
226
- const allFiles = Array.from(allFilesMap.values());
227
247
 
228
- console.log(` Found ${allFiles.length} files to sync`);
229
-
230
- if (dryRun) {
231
- console.log("\nšŸ“‹ Files that would be synced:");
232
- for (const f of allFiles) {
233
- console.log(` ${f.localPath} → ${f.remotePath}`);
234
- }
248
+ // Re-collect language-referenced files on every push (if language.json is in config.files)
249
+ // so that new language term sources are picked up without needing --init again.
250
+ const languageFiles = await collectLanguageFilesIfConfigured(configFiles, projectName);
251
+ if (languageFiles.length > 0) {
252
+ console.log(` + ${languageFiles.length} language-referenced file(s) to sync`);
235
253
  }
236
254
 
237
- // Step 2: Build the config.files array for all synced files
238
- const configFilesEntries = allFiles.map((f) => ({
239
- remotePath: f.remotePath,
240
- localPath: f.downloadLocalPath ?? f.localPath,
241
- direction: "download" as const,
242
- }));
243
-
244
- // Step 3: Update config.files and save
245
- console.log("\nšŸ’¾ Updating config.files with sync entries...");
246
- if (!dryRun) {
247
- // Preserve any existing files entries not in our set
248
- const existingFiles = config.files || [];
249
- const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
250
-
251
- // Keep entries that don't overlap with new ones
252
- const preserved = existingFiles.filter(
253
- (e) => !newRemotePaths.has(e.remotePath)
254
- );
255
+ // Merge language files into the upload list (deduplicate by remotePath)
256
+ const allFilesMap = new Map<string, { remotePath: string; localPath: string }>();
257
+ for (const f of configFiles) {
258
+ allFilesMap.set(f.remotePath, f);
259
+ }
260
+ for (const f of languageFiles) {
261
+ const entry = { remotePath: f.remotePath, localPath: f.downloadLocalPath ?? f.localPath };
262
+ allFilesMap.set(f.remotePath, entry);
263
+ }
264
+ const allFiles = Array.from(allFilesMap.values());
255
265
 
256
- config.files = [...preserved, ...configFilesEntries];
266
+ // If new language files were found, update config.files so they persist
267
+ if (languageFiles.length > 0 && !dryRun) {
268
+ config.files = allFiles.map((f) => ({ ...f, direction: "download" as const }));
257
269
  await updateConfig(config);
258
- console.log(` āœ“ Updated config with ${config.files.length} file entries`);
259
- } else {
260
- console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
261
270
  }
262
271
 
263
- // Step 4: Build workerConfigJson
264
- const workerConfigJson = buildWorkerConfigJson(config, configFilesEntries);
272
+ // Build the workerConfigJson using the full file list
273
+ const workerConfigJson = buildWorkerConfigJson(config, allFiles.map((f) => ({ ...f, direction: "download" })));
265
274
 
266
- // Step 5: Upload all files
267
- console.log(`\nšŸš€ Uploading ${allFiles.length} files...`);
275
+ // Upload all files
276
+ console.log(`\nšŸš€ Uploading ${allFiles.length} configured files...`);
268
277
  let successCount = 0;
269
278
  let failCount = 0;
270
279
 
271
- for (const file of allFiles) {
280
+ for (const mount of allFiles) {
281
+ const { remotePath, localPath } = mount;
272
282
  try {
273
- if (file.isDirectory) {
274
- // Upload all files recursively in the local directory
275
- const localDir = file.localPath.endsWith("/") ? file.localPath : file.localPath + "/";
276
- const remoteDir = file.remotePath.endsWith("/") ? file.remotePath : file.remotePath + "/";
277
- const relFiles = listFilesRecursively(localDir);
278
- console.log(` šŸ“ Uploading directory ${localDir} → ${remoteDir} (${relFiles.length} files)`);
279
- for (const relFile of relFiles) {
280
- await uploadSingleFile(client, AwsS3, localDir + relFile, remoteDir + relFile, dryRun);
281
- successCount++;
282
- }
283
+ if (remotePath.endsWith("/") || localPath.endsWith("/")) {
284
+ const count = await uploadDirectory(client, AwsS3, remotePath, localPath, dryRun);
285
+ successCount += count;
283
286
  } else {
284
- await uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
287
+ await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
285
288
  successCount++;
286
289
  }
287
290
  } catch (error) {
288
- console.error(` āŒ Failed to upload ${file.localPath}: ${error.message}`);
291
+ console.error(` āŒ Failed to upload ${localPath}: ${error.message}`);
289
292
  failCount++;
290
293
  }
291
294
  }
292
295
 
293
296
  console.log(`\n āœ“ Upload complete: ${successCount} succeeded, ${failCount} failed`);
294
297
 
295
- // Step 6: Create or update cloud worker
298
+ // Create or update cloud worker
296
299
  if (create) {
297
300
  const workerName = name || `${projectName}-worker`;
298
301
  console.log(`\nšŸŒ©ļø Creating cloud worker "${workerName}"...`);
@@ -330,3 +333,62 @@ export async function cloudWorker(options: CloudWorkerOptions) {
330
333
  console.log(`\nāœ… Cloud worker sync complete!`);
331
334
  }
332
335
  }
336
+
337
+ /**
338
+ * Pull the latest workerConfigJson from the cloud worker API and update the
339
+ * local knowhow.json config to match.
340
+ */
341
+ export async function pullCloudWorkerConfig(options: CloudWorkerPullOptions) {
342
+ const { id, apiUrl = KNOWHOW_API_URL } = options;
343
+
344
+ // Load JWT
345
+ const jwt = await loadJwt();
346
+ if (!jwt) {
347
+ console.error("āŒ No JWT token found. Please run 'knowhow login' first.");
348
+ process.exit(1);
349
+ }
350
+
351
+ const client = new KnowhowSimpleClient(apiUrl, jwt);
352
+
353
+ console.log(`šŸ”„ Pulling config for cloud worker ${id}...`);
354
+
355
+ const resp = await client.getCloudWorker(id);
356
+ const remoteWorker = resp.data;
357
+
358
+ if (!remoteWorker) {
359
+ console.error(`āŒ Cloud worker ${id} not found.`);
360
+ process.exit(1);
361
+ }
362
+
363
+ const remoteConfig = (remoteWorker.workerConfigJson ?? {}) as {
364
+ mcps?: McpConfig[];
365
+ modules?: string[];
366
+ plugins?: Config["plugins"];
367
+ agents?: Config["agents"];
368
+ };
369
+
370
+ // Load current local config
371
+ const localConfig = await getConfig();
372
+
373
+ // Merge remote fields into local config
374
+ if (remoteConfig.mcps !== undefined) {
375
+ localConfig.mcps = remoteConfig.mcps;
376
+ }
377
+ if (remoteConfig.modules !== undefined) {
378
+ localConfig.modules = remoteConfig.modules;
379
+ }
380
+ if (remoteConfig.plugins !== undefined) {
381
+ localConfig.plugins = remoteConfig.plugins;
382
+ }
383
+ if (remoteConfig.agents !== undefined) {
384
+ localConfig.agents = remoteConfig.agents;
385
+ }
386
+
387
+ await updateConfig(localConfig);
388
+
389
+ const mcpCount = remoteConfig.mcps?.length ?? 0;
390
+ console.log(`āœ… Config pulled! ${mcpCount} MCP(s) now configured locally.`);
391
+ console.log(` Run 'knowhow worker' or trigger reloadConfig to apply changes.`);
392
+
393
+ return { mcps: remoteConfig.mcps, modules: remoteConfig.modules };
394
+ }