@tyvm/knowhow 0.0.107 → 0.0.108-dev.126b29e
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.
- package/README.md +45 -0
- package/package.json +8 -2
- package/scripts/publish.sh +86 -0
- package/src/agents/base/base.ts +1 -0
- package/src/agents/tools/execCommand.ts +49 -6
- package/src/chat/modules/AgentModule.ts +55 -30
- package/src/chat/modules/SessionsModule.ts +7 -2
- package/src/cli.ts +7 -2
- package/src/clients/anthropic.ts +19 -16
- package/src/clients/types.ts +11 -0
- package/src/cloudWorker.ts +75 -1
- package/src/index.ts +17 -0
- package/src/plugins/embedding.ts +11 -6
- package/src/plugins/vim.ts +5 -16
- package/src/services/KnowhowClient.ts +22 -2
- package/src/services/S3.ts +10 -0
- package/src/services/modules/types.ts +2 -0
- package/src/worker.ts +65 -1
- package/src/workers/tools/index.ts +2 -0
- package/src/workers/tools/reloadConfig.ts +84 -0
- package/tests/services/WorkerReloadConfig.test.ts +141 -0
- package/ts_build/package.json +8 -2
- package/ts_build/src/agents/base/base.js +1 -0
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +39 -5
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +39 -19
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +7 -2
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/cli.js +8 -2
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +5 -5
- package/ts_build/src/clients/anthropic.js +19 -16
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +3 -0
- package/ts_build/src/cloudWorker.d.ts +9 -0
- package/ts_build/src/cloudWorker.js +36 -0
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/index.js +14 -0
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/plugins/embedding.js +4 -3
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/vim.js +3 -9
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +12 -0
- package/ts_build/src/services/KnowhowClient.js +11 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/S3.js +7 -0
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/src/worker.js +38 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/tools/index.d.ts +2 -0
- package/ts_build/src/workers/tools/index.js +4 -1
- package/ts_build/src/workers/tools/index.js.map +1 -1
- package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
- package/ts_build/src/workers/tools/reloadConfig.js +48 -0
- package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
package/src/cloudWorker.ts
CHANGED
|
@@ -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);
|
package/src/plugins/embedding.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
75
|
-
${JSON.stringify(
|
|
79
|
+
return `EMBEDDING PLUGIN: Our knowledgebase indicates these embedding entries may be related to the question:
|
|
80
|
+
${JSON.stringify(ids)}`;
|
|
76
81
|
}
|
|
77
|
-
}
|
|
82
|
+
}
|
package/src/plugins/vim.ts
CHANGED
|
@@ -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 =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
}
|
|
@@ -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
|
*/
|
package/src/services/S3.ts
CHANGED
|
@@ -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 },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Tool } from "../../clients/types";
|
|
2
|
+
import { getConfig } from "../../config";
|
|
3
|
+
import { McpServerService } from "../../services/McpServer";
|
|
4
|
+
import { McpService } from "../../services/Mcp";
|
|
5
|
+
import { ToolsService } from "../../services/Tools";
|
|
6
|
+
|
|
7
|
+
export interface ReloadConfigResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
toolCount: number;
|
|
10
|
+
mcpCount: number;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Factory that creates the reloadConfig tool with access to runtime services.
|
|
16
|
+
* The tool re-reads the config from disk, reconnects all MCPs, and rebuilds
|
|
17
|
+
* the active tool list — the same logic as the WebSocket reloadConfig handler.
|
|
18
|
+
*
|
|
19
|
+
* Typical usage after pulling updated config from the cloud worker API:
|
|
20
|
+
* 1. execCommand("knowhow cloudworker --pull <cloudWorkerId>")
|
|
21
|
+
* 2. reloadConfig()
|
|
22
|
+
*/
|
|
23
|
+
export function makeReloadConfigTool(
|
|
24
|
+
Mcp: McpService,
|
|
25
|
+
Tools: ToolsService,
|
|
26
|
+
mcpServer: McpServerService,
|
|
27
|
+
setToolsToUse: (tools: ReturnType<typeof Tools.getToolsByNames>) => void
|
|
28
|
+
) {
|
|
29
|
+
const reloadConfig = async (): Promise<ReloadConfigResult> => {
|
|
30
|
+
try {
|
|
31
|
+
// Re-read fresh config from disk
|
|
32
|
+
const freshConfig = await getConfig();
|
|
33
|
+
|
|
34
|
+
// Close all existing MCP connections
|
|
35
|
+
await Mcp.closeAll();
|
|
36
|
+
|
|
37
|
+
// Reconnect from fresh config and re-register tools
|
|
38
|
+
await Mcp.connectToConfigured(Tools);
|
|
39
|
+
|
|
40
|
+
// Rebuild the allowed tools list from fresh config
|
|
41
|
+
const allowedToolNames =
|
|
42
|
+
freshConfig.worker?.allowedTools ?? Tools.getToolNames();
|
|
43
|
+
const newToolsToUse = Tools.getToolsByNames(allowedToolNames);
|
|
44
|
+
setToolsToUse(newToolsToUse);
|
|
45
|
+
|
|
46
|
+
// Update the MCP server with the new tool list
|
|
47
|
+
mcpServer.withTools(newToolsToUse);
|
|
48
|
+
|
|
49
|
+
const mcpCount = freshConfig.mcps?.length ?? 0;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
toolCount: newToolsToUse.length,
|
|
54
|
+
mcpCount,
|
|
55
|
+
message: `Config reloaded: ${newToolsToUse.length} tools active, ${mcpCount} MCP(s) configured`,
|
|
56
|
+
};
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
toolCount: 0,
|
|
62
|
+
mcpCount: 0,
|
|
63
|
+
message: `Failed to reload config: ${message}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const reloadConfigDefinition: Tool = {
|
|
69
|
+
type: "function" as const,
|
|
70
|
+
function: {
|
|
71
|
+
name: "reloadConfig",
|
|
72
|
+
description:
|
|
73
|
+
"Reload the worker config from disk, reconnect all MCPs, and rebuild the active tool list. " +
|
|
74
|
+
"Call this after running `knowhow cloudworker --pull <id>` to apply updated MCPs without restarting the worker.",
|
|
75
|
+
parameters: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {},
|
|
78
|
+
required: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { reloadConfig, reloadConfigDefinition };
|
|
84
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the worker reloadConfig message handler.
|
|
3
|
+
*
|
|
4
|
+
* We test the core logic in isolation — simulating the "reloadConfig" message
|
|
5
|
+
* arriving on the WebSocket and verifying that MCPs are torn down and
|
|
6
|
+
* re-connected, and that the tool list is rebuilt from fresh config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { McpService } from "../../src/services/Mcp";
|
|
10
|
+
import { ToolsService } from "../../src/services/Tools";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers: build the same reload logic that lives in worker.ts so we can
|
|
14
|
+
// test it in isolation without spinning up a real WebSocket server.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
async function simulateReloadConfig(
|
|
18
|
+
Mcp: McpService,
|
|
19
|
+
Tools: ToolsService,
|
|
20
|
+
mcpServer: { withTools: (tools: unknown[]) => void },
|
|
21
|
+
getConfig: () => Promise<{ worker?: { allowedTools?: string[] } }>,
|
|
22
|
+
toolsToUseRef: { value: unknown[] }
|
|
23
|
+
) {
|
|
24
|
+
// This mirrors the handler in worker.ts
|
|
25
|
+
const freshConfig = await getConfig();
|
|
26
|
+
await Mcp.closeAll();
|
|
27
|
+
await Mcp.connectToConfigured(Tools);
|
|
28
|
+
const allowedToolNames =
|
|
29
|
+
freshConfig.worker?.allowedTools ?? Tools.getToolNames();
|
|
30
|
+
toolsToUseRef.value = Tools.getToolsByNames(allowedToolNames);
|
|
31
|
+
mcpServer.withTools(toolsToUseRef.value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tests
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe("Worker reloadConfig handler", () => {
|
|
39
|
+
let Mcp: McpService;
|
|
40
|
+
let Tools: ToolsService;
|
|
41
|
+
let mcpServer: { withTools: jest.Mock };
|
|
42
|
+
let toolsToUseRef: { value: unknown[] };
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
Mcp = new McpService();
|
|
46
|
+
Tools = new ToolsService();
|
|
47
|
+
mcpServer = { withTools: jest.fn() };
|
|
48
|
+
toolsToUseRef = { value: [] };
|
|
49
|
+
|
|
50
|
+
// Spy on MCP methods
|
|
51
|
+
jest.spyOn(Mcp, "closeAll").mockResolvedValue(undefined);
|
|
52
|
+
jest.spyOn(Mcp, "connectToConfigured").mockResolvedValue(undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should call closeAll() to tear down existing MCP connections", async () => {
|
|
60
|
+
const getConfig = jest
|
|
61
|
+
.fn()
|
|
62
|
+
.mockResolvedValue({ worker: { allowedTools: [] } });
|
|
63
|
+
|
|
64
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
65
|
+
|
|
66
|
+
expect(Mcp.closeAll).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should call connectToConfigured() to reconnect MCPs from fresh config", async () => {
|
|
70
|
+
const getConfig = jest
|
|
71
|
+
.fn()
|
|
72
|
+
.mockResolvedValue({ worker: { allowedTools: [] } });
|
|
73
|
+
|
|
74
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
75
|
+
|
|
76
|
+
expect(Mcp.connectToConfigured).toHaveBeenCalledWith(Tools);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should rebuild the tool list from allowedTools in fresh config", async () => {
|
|
80
|
+
// Spy on getToolsByNames so we can track what names were requested
|
|
81
|
+
const toolsByNamesSpy = jest
|
|
82
|
+
.spyOn(Tools, "getToolsByNames")
|
|
83
|
+
.mockReturnValue([{ function: { name: "execCommand" } }] as ReturnType<ToolsService["getToolsByNames"]>);
|
|
84
|
+
|
|
85
|
+
const getConfig = jest
|
|
86
|
+
.fn()
|
|
87
|
+
.mockResolvedValue({ worker: { allowedTools: ["execCommand"] } });
|
|
88
|
+
|
|
89
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
90
|
+
|
|
91
|
+
expect(toolsByNamesSpy).toHaveBeenCalledWith(["execCommand"]);
|
|
92
|
+
expect(toolsToUseRef.value).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should fall back to all tool names when allowedTools is not set", async () => {
|
|
96
|
+
const allNames = ["execCommand", "readFile", "writeFileChunk"];
|
|
97
|
+
jest.spyOn(Tools, "getToolNames").mockReturnValue(allNames);
|
|
98
|
+
const toolsByNamesSpy = jest
|
|
99
|
+
.spyOn(Tools, "getToolsByNames")
|
|
100
|
+
.mockReturnValue([] as ReturnType<ToolsService["getToolsByNames"]>);
|
|
101
|
+
|
|
102
|
+
// Config has no worker.allowedTools
|
|
103
|
+
const getConfig = jest.fn().mockResolvedValue({});
|
|
104
|
+
|
|
105
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
106
|
+
|
|
107
|
+
expect(toolsByNamesSpy).toHaveBeenCalledWith(allNames);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should call mcpServer.withTools() with the rebuilt tool list", async () => {
|
|
111
|
+
const fakeTools = [{ function: { name: "readFile" } }] as ReturnType<ToolsService["getToolsByNames"]>;
|
|
112
|
+
jest.spyOn(Tools, "getToolsByNames").mockReturnValue(fakeTools);
|
|
113
|
+
|
|
114
|
+
const getConfig = jest
|
|
115
|
+
.fn()
|
|
116
|
+
.mockResolvedValue({ worker: { allowedTools: ["readFile"] } });
|
|
117
|
+
|
|
118
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
119
|
+
|
|
120
|
+
expect(mcpServer.withTools).toHaveBeenCalledWith(fakeTools);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should re-read the config on every reload (not use stale config)", async () => {
|
|
124
|
+
const getConfig = jest
|
|
125
|
+
.fn()
|
|
126
|
+
.mockResolvedValueOnce({ worker: { allowedTools: ["execCommand"] } })
|
|
127
|
+
.mockResolvedValueOnce({ worker: { allowedTools: ["readFile", "writeFileChunk"] } });
|
|
128
|
+
|
|
129
|
+
jest.spyOn(Tools, "getToolsByNames").mockReturnValue([]);
|
|
130
|
+
|
|
131
|
+
// First reload
|
|
132
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
133
|
+
// Second reload
|
|
134
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
135
|
+
|
|
136
|
+
expect(getConfig).toHaveBeenCalledTimes(2);
|
|
137
|
+
// Each reload should tear down and reconnect
|
|
138
|
+
expect(Mcp.closeAll).toHaveBeenCalledTimes(2);
|
|
139
|
+
expect(Mcp.connectToConfigured).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.108-dev.126b29e",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,13 @@
|
|
|
21
21
|
"lint": "tslint ./src/**/*.ts",
|
|
22
22
|
"lint:deps": "depcheck --config=.depcheckrc",
|
|
23
23
|
"lint:all": "npm run lint && npm run lint:deps",
|
|
24
|
-
"check:model-pricing": "ts-node scripts/check-model-pricing.ts"
|
|
24
|
+
"check:model-pricing": "ts-node scripts/check-model-pricing.ts",
|
|
25
|
+
"version:nightly": "bash scripts/publish.sh version:nightly",
|
|
26
|
+
"version:dev": "bash scripts/publish.sh version:dev",
|
|
27
|
+
"publish:nightly": "bash scripts/publish.sh nightly",
|
|
28
|
+
"publish:dev": "bash scripts/publish.sh dev",
|
|
29
|
+
"publish:latest": "bash scripts/publish.sh stable",
|
|
30
|
+
"publish:stable": "bash scripts/publish.sh stable"
|
|
25
31
|
},
|
|
26
32
|
"keywords": [],
|
|
27
33
|
"author": "Micah Riggan",
|
|
@@ -486,6 +486,7 @@ class BaseAgent {
|
|
|
486
486
|
messages,
|
|
487
487
|
tools: this.getEnabledTools(),
|
|
488
488
|
tool_choice: "auto",
|
|
489
|
+
long_ttl_cache: this.runTime() > 300_000,
|
|
489
490
|
});
|
|
490
491
|
if (this.status === this.eventTypes.pause) {
|
|
491
492
|
this.log("Agent was paused after completion, waiting before processing tool calls");
|