@tyvm/knowhow 0.0.107 → 0.0.108-dev.879609c

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 (84) hide show
  1. package/README.md +45 -0
  2. package/package.json +8 -2
  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/chat/CliChatService.ts +7 -1
  7. package/src/chat/modules/AgentModule.ts +55 -30
  8. package/src/chat/modules/SessionsModule.ts +7 -2
  9. package/src/chat/renderer/CompactRenderer.ts +20 -0
  10. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  11. package/src/chat/renderer/FancyRenderer.ts +19 -0
  12. package/src/chat/renderer/types.ts +11 -0
  13. package/src/cli.ts +7 -2
  14. package/src/clients/anthropic.ts +19 -16
  15. package/src/clients/types.ts +23 -4
  16. package/src/cloudWorker.ts +75 -1
  17. package/src/index.ts +17 -0
  18. package/src/plugins/embedding.ts +11 -6
  19. package/src/plugins/vim.ts +5 -16
  20. package/src/processors/JsonCompressor.ts +3 -3
  21. package/src/services/KnowhowClient.ts +22 -2
  22. package/src/services/S3.ts +10 -0
  23. package/src/services/modules/types.ts +2 -0
  24. package/src/worker.ts +65 -1
  25. package/src/workers/tools/index.ts +2 -0
  26. package/src/workers/tools/reloadConfig.ts +84 -0
  27. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  28. package/ts_build/package.json +8 -2
  29. package/ts_build/src/agents/base/base.js +11 -0
  30. package/ts_build/src/agents/base/base.js.map +1 -1
  31. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  32. package/ts_build/src/agents/tools/execCommand.js +39 -5
  33. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  34. package/ts_build/src/chat/CliChatService.js +10 -1
  35. package/ts_build/src/chat/CliChatService.js.map +1 -1
  36. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  37. package/ts_build/src/chat/modules/AgentModule.js +39 -19
  38. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  39. package/ts_build/src/chat/modules/SessionsModule.js +7 -2
  40. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  41. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  42. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  43. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  44. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  45. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  46. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  47. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  48. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  49. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  50. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  51. package/ts_build/src/cli.js +8 -2
  52. package/ts_build/src/cli.js.map +1 -1
  53. package/ts_build/src/clients/anthropic.d.ts +5 -5
  54. package/ts_build/src/clients/anthropic.js +19 -16
  55. package/ts_build/src/clients/anthropic.js.map +1 -1
  56. package/ts_build/src/clients/types.d.ts +5 -2
  57. package/ts_build/src/cloudWorker.d.ts +9 -0
  58. package/ts_build/src/cloudWorker.js +36 -0
  59. package/ts_build/src/cloudWorker.js.map +1 -1
  60. package/ts_build/src/index.js +14 -0
  61. package/ts_build/src/index.js.map +1 -1
  62. package/ts_build/src/plugins/embedding.js +4 -3
  63. package/ts_build/src/plugins/embedding.js.map +1 -1
  64. package/ts_build/src/plugins/vim.js +3 -9
  65. package/ts_build/src/plugins/vim.js.map +1 -1
  66. package/ts_build/src/processors/JsonCompressor.js +3 -3
  67. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  68. package/ts_build/src/services/KnowhowClient.d.ts +12 -0
  69. package/ts_build/src/services/KnowhowClient.js +11 -0
  70. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  71. package/ts_build/src/services/S3.js +7 -0
  72. package/ts_build/src/services/S3.js.map +1 -1
  73. package/ts_build/src/services/modules/types.d.ts +2 -0
  74. package/ts_build/src/worker.js +38 -0
  75. package/ts_build/src/worker.js.map +1 -1
  76. package/ts_build/src/workers/tools/index.d.ts +2 -0
  77. package/ts_build/src/workers/tools/index.js +4 -1
  78. package/ts_build/src/workers/tools/index.js.map +1 -1
  79. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  80. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  81. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  82. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  83. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  84. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
@@ -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
  ]
@@ -413,6 +414,8 @@ export class GenericAnthropicClient implements GenericClient {
413
414
 
414
415
  model: options.model,
415
416
  usage: response.usage ? {
417
+ input_tokens: response.usage.input_tokens ?? 0,
418
+ output_tokens: response.usage.output_tokens ?? 0,
416
419
  prompt_tokens: response.usage.input_tokens ?? 0,
417
420
  completion_tokens: response.usage.output_tokens ?? 0,
418
421
  total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
@@ -422,7 +425,7 @@ export class GenericAnthropicClient implements GenericClient {
422
425
  usd_cost: this.calculateCost(options.model, response.usage),
423
426
  };
424
427
  } catch (err) {
425
- if ("headers" in err && err.headers["x-should-retry"] === "true") {
428
+ if ("headers" in err && err.headers?.["x-should-retry"] === "true") {
426
429
  console.warn("Retrying failed request", err);
427
430
  await wait(2500);
428
431
  return this.createChatCompletion(options);
@@ -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
  /**
@@ -73,6 +86,10 @@ export interface TokenUsage {
73
86
  prompt_tokens: number;
74
87
  /** Total output/completion tokens generated */
75
88
  completion_tokens: number;
89
+ /** Alternative field name for input tokens (some providers use this) */
90
+ input_tokens?: number;
91
+ /** Alternative field name for output tokens (some providers use this) */
92
+ output_tokens?: number;
76
93
  /** Convenience total (prompt + completion) */
77
94
  total_tokens?: number;
78
95
  /** Cache details */
@@ -290,7 +307,9 @@ export interface GenericClient {
290
307
  * When modality is provided, return only models for that modality (static list).
291
308
  * When omitted, return ALL models (backward compat — may do a live API call).
292
309
  */
293
- getModels(modality?: ModelModality): Promise<{ id: string; modality?: ModelModality[] }[]>;
310
+ getModels(
311
+ modality?: ModelModality
312
+ ): Promise<{ id: string; modality?: ModelModality[] }[]>;
294
313
  /**
295
314
  * Returns the context window limit and compression threshold for a given model,
296
315
  * or undefined if the model is not known to this client.
@@ -4,9 +4,14 @@ 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";
7
+ import { Language, Config, McpConfig } from "./types";
8
8
  import { S3Service } from "./services/S3";
9
9
 
10
+ export interface CloudWorkerPullOptions {
11
+ id: string;
12
+ apiUrl?: string;
13
+ }
14
+
10
15
  export interface CloudWorkerOptions {
11
16
  create?: boolean;
12
17
  push?: string; // uid of existing cloud worker
@@ -330,3 +335,72 @@ export async function cloudWorker(options: CloudWorkerOptions) {
330
335
  console.log(`\n✅ Cloud worker sync complete!`);
331
336
  }
332
337
  }
338
+
339
+ /**
340
+ * Pull the latest workerConfigJson from the cloud worker API and update the
341
+ * local knowhow.json config to match.
342
+ *
343
+ * This is the "pull" half of the config sync cycle. After running this,
344
+ * you can reload the worker's MCPs (in-process) via the reloadConfig
345
+ * WebSocket message or by calling `knowhow worker` again.
346
+ *
347
+ * Merged fields from workerConfigJson:
348
+ * - mcps → overwrites config.mcps
349
+ * - modules → overwrites config.modules (optional, only if present)
350
+ * - plugins → overwrites config.plugins (optional, only if present)
351
+ * - agents → overwrites config.agents (optional, only if present)
352
+ */
353
+ export async function pullCloudWorkerConfig(options: CloudWorkerPullOptions) {
354
+ const { id, apiUrl = KNOWHOW_API_URL } = options;
355
+
356
+ // Load JWT
357
+ const jwt = await loadJwt();
358
+ if (!jwt) {
359
+ console.error("❌ No JWT token found. Please run 'knowhow login' first.");
360
+ process.exit(1);
361
+ }
362
+
363
+ const client = new KnowhowSimpleClient(apiUrl, jwt);
364
+
365
+ console.log(`🔄 Pulling config for cloud worker ${id}...`);
366
+
367
+ const resp = await client.getCloudWorker(id);
368
+ const remoteWorker = resp.data;
369
+
370
+ if (!remoteWorker) {
371
+ console.error(`❌ Cloud worker ${id} not found.`);
372
+ process.exit(1);
373
+ }
374
+
375
+ const remoteConfig = (remoteWorker.workerConfigJson ?? {}) as {
376
+ mcps?: McpConfig[];
377
+ modules?: string[];
378
+ plugins?: Config["plugins"];
379
+ agents?: Config["agents"];
380
+ };
381
+
382
+ // Load current local config
383
+ const localConfig = await getConfig();
384
+
385
+ // Merge remote fields into local config
386
+ if (remoteConfig.mcps !== undefined) {
387
+ localConfig.mcps = remoteConfig.mcps;
388
+ }
389
+ if (remoteConfig.modules !== undefined) {
390
+ localConfig.modules = remoteConfig.modules;
391
+ }
392
+ if (remoteConfig.plugins !== undefined) {
393
+ localConfig.plugins = remoteConfig.plugins;
394
+ }
395
+ if (remoteConfig.agents !== undefined) {
396
+ localConfig.agents = remoteConfig.agents;
397
+ }
398
+
399
+ await updateConfig(localConfig);
400
+
401
+ const mcpCount = remoteConfig.mcps?.length ?? 0;
402
+ console.log(`✅ Config pulled! ${mcpCount} MCP(s) now configured locally.`);
403
+ console.log(` Run 'knowhow worker' or trigger reloadConfig to apply changes.`);
404
+
405
+ return { mcps: remoteConfig.mcps, modules: remoteConfig.modules };
406
+ }
package/src/index.ts CHANGED
@@ -138,6 +138,23 @@ export async function upload() {
138
138
  if (!source.remoteId) {
139
139
  throw new Error("remoteId is required for knowhow uploads");
140
140
  }
141
+ // Warn if the local embeddingModel differs from the one stored on the backend
142
+ try {
143
+ const remoteEmbedding = await knowhowApiClient.getOrgEmbedding(source.remoteId);
144
+ const localModel = config.embeddingModel || EmbeddingModels.openai.EmbeddingAda2;
145
+ const remoteModel = remoteEmbedding?.modelName;
146
+ if (remoteModel && remoteModel !== localModel) {
147
+ console.warn(
148
+ `⚠️ WARNING: Embedding model mismatch for "${remoteEmbedding.name}" (remoteId: ${source.remoteId}).\n` +
149
+ ` Local config.embeddingModel: ${localModel}\n` +
150
+ ` Backend embedding modelName: ${remoteModel}\n` +
151
+ ` Vectors generated with different models are not comparable — search results will be incorrect.\n` +
152
+ ` Update your config.embeddingModel to "${remoteModel}" or update the backend embedding to "${localModel}".`
153
+ );
154
+ }
155
+ } catch (e) {
156
+ // Non-fatal — don't block upload if metadata fetch fails
157
+ }
141
158
  const url = await knowhowApiClient.getPresignedUploadUrl(source);
142
159
  console.log("Uploading to", url);
143
160
  await AwsS3.uploadToPresignedUrl(url, source.output);
@@ -20,9 +20,12 @@ export class EmbeddingPlugin extends PluginBase {
20
20
 
21
21
  constructor(context) {
22
22
  super(context);
23
-
23
+
24
24
  // Subscribe to file:post-edit events
25
- this.context.Events.on("file:post-edit", this.handleFilePostEdit.bind(this));
25
+ this.context.Events.on(
26
+ "file:post-edit",
27
+ this.handleFilePostEdit.bind(this)
28
+ );
26
29
  }
27
30
 
28
31
  async embed() {
@@ -68,10 +71,12 @@ export class EmbeddingPlugin extends PluginBase {
68
71
  this.log(`Reading entry ${entry.id}`);
69
72
  }
70
73
 
71
- const contextLength = JSON.stringify(context).split(" ").length;
74
+ const ids = context.map((entry) => entry.id);
75
+
76
+ const contextLength = JSON.stringify(ids).split(" ").length;
72
77
  this.log(`Found ${context.length} entries. Loading ${contextLength} words`);
73
78
 
74
- return `EMBEDDING PLUGIN: Our knowledgebase contains this information which can be used to answer the question:
75
- ${JSON.stringify(context)}`;
79
+ return `EMBEDDING PLUGIN: Our knowledgebase indicates these embedding entries may be related to the question:
80
+ ${JSON.stringify(ids)}`;
76
81
  }
77
- }
82
+ }
@@ -73,22 +73,11 @@ export class VimPlugin extends PluginBase {
73
73
 
74
74
  async call() {
75
75
  const vimFiles = await this.getVimFiles();
76
- const fileContents = await Promise.all(
77
- vimFiles.map(async (f) => {
78
- const loaded = await this.getFileContents(f);
79
-
80
- const preview =
81
- loaded.content.length > 1000
82
- ? loaded.content.slice(0, 1000) +
83
- "... file trimmed, read file for full content"
84
- : loaded.content;
85
-
86
- return {
87
- sourceFile: loaded.filePath,
88
- content: loaded.content.slice(0, 1000),
89
- };
90
- })
91
- );
76
+ const fileContents = vimFiles.map((f) => {
77
+ return {
78
+ sourceFile: f,
79
+ };
80
+ });
92
81
  if (fileContents.length === 0) {
93
82
  return "VIM PLUGIN: No files open in vim";
94
83
  }
@@ -382,7 +382,7 @@ export class JsonCompressor {
382
382
  i + currentChunk.length - 1
383
383
  }]\nPreview: ${chunkString.substring(0, 100)}...\n[Use ${
384
384
  this.toolName
385
- } tool with key "${key}" to retrieve this chunk]`;
385
+ } tool with key "${key}" to retrieve this chunk]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
386
386
  finalArray.unshift(stub); // Add stub to the start of our final result.
387
387
 
388
388
  currentChunk = [];
@@ -453,7 +453,7 @@ export class JsonCompressor {
453
453
  result
454
454
  ).join(", ")}\nPreview: ${objectAsString.substring(0, 200)}...\n[Use ${
455
455
  this.toolName
456
- } tool with key "${key}" to retrieve full content]`;
456
+ } tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
457
457
  }
458
458
  return result;
459
459
  }
@@ -486,7 +486,7 @@ export class JsonCompressor {
486
486
  200
487
487
  )}...\n[Use ${
488
488
  this.toolName
489
- } tool with key "${key}" to retrieve full content]`;
489
+ } tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
490
490
  }
491
491
  return obj;
492
492
  }
@@ -171,11 +171,9 @@ export class KnowhowSimpleClient {
171
171
  try {
172
172
  this.jwtValidated = true;
173
173
  const response = await this.me();
174
-
175
174
  const user = response.data.user;
176
175
  const orgs = user.orgs;
177
176
  const orgId = response.data.orgId;
178
-
179
177
  const currentOrg = orgs.find((org) => {
180
178
  return org.organizationId === orgId;
181
179
  });
@@ -227,6 +225,17 @@ export class KnowhowSimpleClient {
227
225
  return presignedUrl;
228
226
  }
229
227
 
228
+ async getOrgEmbedding(id: string) {
229
+ await this.checkJwt();
230
+ const resp = await http.get(
231
+ `${this.baseUrl}/api/org-embeddings/${id}`,
232
+ {
233
+ headers: this.headers,
234
+ }
235
+ );
236
+ return resp.data as { id: string; modelName: string; name: string; [key: string]: unknown };
237
+ }
238
+
230
239
  async updateEmbeddingMetadata(
231
240
  id: string,
232
241
  data: {
@@ -718,6 +727,17 @@ export class KnowhowSimpleClient {
718
727
  );
719
728
  }
720
729
 
730
+ /**
731
+ * Get a single cloud worker by ID
732
+ */
733
+ async getCloudWorker(id: string) {
734
+ await this.checkJwt();
735
+ return http.get<{ id: string; name: string; status: string; workerConfigJson?: Record<string, unknown> }>(
736
+ `${this.baseUrl}/api/cloud-workers/${id}`,
737
+ { headers: this.headers }
738
+ );
739
+ }
740
+
721
741
  /**
722
742
  * Update an existing cloud worker
723
743
  */
@@ -1,5 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import { createWriteStream, createReadStream } from "fs";
3
+ import * as crypto from "crypto";
3
4
  import { pipeline, Readable } from "stream";
4
5
  import * as util from "util";
5
6
 
@@ -14,10 +15,19 @@ export class S3Service {
14
15
  const fileContent = fs.readFileSync(filePath);
15
16
  const fileStats = await fs.promises.stat(filePath);
16
17
 
18
+ // Compute SHA-256 checksum (base64) — required when presigned URL was
19
+ // generated with ChecksumAlgorithm: SHA256
20
+ const sha256Base64 = crypto
21
+ .createHash("sha256")
22
+ .update(fileContent)
23
+ .digest("base64");
24
+
17
25
  const response = await fetch(presignedUrl, {
18
26
  method: "PUT",
19
27
  headers: {
20
28
  "Content-Length": String(fileStats.size),
29
+ "x-amz-checksum-sha256": sha256Base64,
30
+ "x-amz-sdk-checksum-algorithm": "SHA256",
21
31
  },
22
32
  body: fileContent,
23
33
  // @ts-ignore
@@ -9,6 +9,7 @@ import { PluginService } from "../../plugins/plugins";
9
9
  import { AIClient } from "../../clients";
10
10
  import { ToolsService } from "../Tools";
11
11
  import { MediaProcessorService } from "../MediaProcessorService";
12
+ import { TunnelHandler } from "@tyvm/knowhow-tunnel";
12
13
 
13
14
  /*
14
15
  *
@@ -53,6 +54,7 @@ export interface ModuleContext {
53
54
  Clients: AIClient;
54
55
  Tools: ToolsService;
55
56
  MediaProcessor?: MediaProcessorService;
57
+ Tunnel?: TunnelHandler;
56
58
  }
57
59
 
58
60
  export interface KnowhowModule {
package/src/worker.ts CHANGED
@@ -6,7 +6,7 @@ import { loadJwt } from "./login";
6
6
  import { services } from "./services";
7
7
  import { PasskeySetupService } from "./workers/auth/PasskeySetup";
8
8
  import { WorkerPasskeyAuthService } from "./workers/auth/WorkerPasskeyAuth";
9
- import { makeUnlockTool, makeLockTool } from "./workers/tools";
9
+ import { makeUnlockTool, makeLockTool, makeReloadConfigTool } from "./workers/tools";
10
10
  import { McpServerService } from "./services/Mcp";
11
11
  import * as allTools from "./agents/tools";
12
12
  import workerTools from "./workers/tools";
@@ -14,6 +14,7 @@ import { wait } from "./utils";
14
14
  import { getConfig, updateConfig } from "./config";
15
15
  import { KNOWHOW_API_URL } from "./services/KnowhowClient";
16
16
  import { registerWorkerPath } from "./workerRegistry";
17
+ import { ModulesService } from "./services/modules";
17
18
 
18
19
  const API_URL = KNOWHOW_API_URL;
19
20
 
@@ -264,6 +265,22 @@ export async function worker(options?: {
264
265
  console.log("🔑 Auth tools registered: unlock, lock");
265
266
  }
266
267
 
268
+ // Register the reloadConfig tool so agents can hot-reload MCPs/config
269
+ // without restarting the worker process.
270
+ // Uses a closure over `toolsToUse` so the tool can update it in-place.
271
+ const { reloadConfig, reloadConfigDefinition } = makeReloadConfigTool(
272
+ Mcp,
273
+ Tools,
274
+ mcpServer,
275
+ (newTools) => {
276
+ toolsToUse = newTools;
277
+ }
278
+ );
279
+ Tools.addFunctions({ reloadConfig });
280
+ toolsToUse = [...toolsToUse, reloadConfigDefinition];
281
+
282
+ console.log("🔄 reloadConfig tool registered");
283
+
267
284
  mcpServer.createServer(clientName, clientVersion).withTools(toolsToUse);
268
285
 
269
286
  let connected = false;
@@ -403,6 +420,33 @@ export async function worker(options?: {
403
420
  console.log(`✅ Worker ID recorded: ${parsed.workerId}`);
404
421
  }
405
422
  }
423
+
424
+ // Hot-reload: re-read config, reconnect MCPs, and rebuild the tool list
425
+ // without restarting the worker process.
426
+ if (parsed?.type === "reloadConfig") {
427
+ console.log("🔄 Received reloadConfig — reloading MCPs, modules and tools...");
428
+ try {
429
+ // Re-read fresh config from disk
430
+ const freshConfig = await getConfig();
431
+
432
+ // Close all existing MCP connections
433
+ await Mcp.closeAll();
434
+
435
+ // Reconnect from fresh config and re-register tools
436
+ await Mcp.connectToConfigured(Tools);
437
+
438
+ // Rebuild the allowed tools list from fresh config
439
+ const allowedToolNames = freshConfig.worker?.allowedTools ?? Tools.getToolNames();
440
+ toolsToUse = Tools.getToolsByNames(allowedToolNames);
441
+
442
+ // Update the MCP server with new tool list
443
+ mcpServer.withTools(toolsToUse);
444
+
445
+ console.log(`✅ Config reloaded: ${toolsToUse.length} tools active`);
446
+ } catch (err) {
447
+ console.error("❌ Failed to reload config:", err);
448
+ }
449
+ }
406
450
  } catch {
407
451
  // Not our message — ignore parse errors
408
452
  }
@@ -462,6 +506,16 @@ export async function worker(options?: {
462
506
  tunnelHandler = createTunnelHandler(tunnelConnection!, tunnelConfig);
463
507
  console.log("🌐 Tunnel handler initialized");
464
508
  console.log(tunnelConfig);
509
+
510
+ // Let modules that need the tunnel handler register their addons now
511
+ const tunnelModulesService = new ModulesService();
512
+ const { Agents, Embeddings, Plugins, Clients, Tools, MediaProcessor } = services();
513
+ tunnelModulesService.loadModulesFromConfig({
514
+ Agents, Embeddings, Plugins, Clients, Tools, MediaProcessor,
515
+ Tunnel: tunnelHandler,
516
+ }).catch((err) => {
517
+ console.error("Failed to load tunnel modules:", err);
518
+ });
465
519
  });
466
520
 
467
521
  tunnelConnection.on("close", (code, reason) => {
@@ -708,6 +762,16 @@ export async function tunnel(options?: {
708
762
  tunnelHandler = createTunnelHandler(tunnelConnection, tunnelConfig);
709
763
  console.log("🌐 Tunnel handler initialized");
710
764
  console.log(tunnelConfig);
765
+
766
+ // Let modules that need the tunnel handler register their addons now
767
+ const tunnelModulesService2 = new ModulesService();
768
+ const { Agents: A2, Embeddings: E2, Plugins: P2, Clients: C2, Tools: T2, MediaProcessor: MP2 } = services();
769
+ tunnelModulesService2.loadModulesFromConfig({
770
+ Agents: A2, Embeddings: E2, Plugins: P2, Clients: C2, Tools: T2, MediaProcessor: MP2,
771
+ Tunnel: tunnelHandler,
772
+ }).catch((err) => {
773
+ console.error("Failed to load tunnel modules:", err);
774
+ });
711
775
  });
712
776
 
713
777
  tunnelConnection.on("close", (code, reason) => {
@@ -2,6 +2,7 @@ export * from "./listAllowedPorts";
2
2
  export * from "./getChallenge";
3
3
  export * from "./unlock";
4
4
  export * from "./lock";
5
+ export * from "./reloadConfig";
5
6
 
6
7
  import {
7
8
  listAllowedPorts,
@@ -11,6 +12,7 @@ import {
11
12
  export { makeGetChallengeTool } from "./getChallenge";
12
13
  export { makeUnlockTool } from "./unlock";
13
14
  export { makeLockTool } from "./lock";
15
+ export { makeReloadConfigTool } from "./reloadConfig";
14
16
 
15
17
  export default {
16
18
  tools: { listAllowedPorts },