@tyvm/knowhow 0.0.64 → 0.0.66
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/package.json +2 -1
- package/src/chat/modules/AgentModule.ts +11 -1
- package/src/cli.ts +5 -4
- package/src/clients/anthropic.ts +86 -65
- package/src/config.ts +7 -0
- package/src/services/AgentSynchronization.ts +27 -10
- package/src/types.ts +11 -0
- package/src/worker.ts +145 -8
- package/src/workers/tools/index.ts +10 -0
- package/src/workers/tools/listAllowedPorts.ts +30 -0
- package/ts_build/package.json +2 -1
- package/ts_build/src/chat/modules/AgentModule.js +7 -1
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +5 -4
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +9 -0
- package/ts_build/src/clients/anthropic.js +47 -31
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/config.js +6 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/services/AgentSynchronization.d.ts +2 -0
- package/ts_build/src/services/AgentSynchronization.js +20 -10
- package/ts_build/src/services/AgentSynchronization.js.map +1 -1
- package/ts_build/src/types.d.ts +11 -0
- package/ts_build/src/types.js +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker/handlers/proxyHandler.d.ts +2 -0
- package/ts_build/src/worker/handlers/proxyHandler.js +41 -0
- package/ts_build/src/worker/handlers/proxyHandler.js.map +1 -0
- package/ts_build/src/worker/tools/index.d.ts +1 -0
- package/ts_build/src/worker/tools/index.js +18 -0
- package/ts_build/src/worker/tools/index.js.map +1 -0
- package/ts_build/src/worker/tools/portForwarding.d.ts +49 -0
- package/ts_build/src/worker/tools/portForwarding.js +173 -0
- package/ts_build/src/worker/tools/portForwarding.js.map +1 -0
- package/ts_build/src/worker/types/proxy.d.ts +18 -0
- package/ts_build/src/worker/types/proxy.js +3 -0
- package/ts_build/src/worker/types/proxy.js.map +1 -0
- package/ts_build/src/worker.js +119 -3
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/tools/index.d.ts +9 -0
- package/ts_build/src/workers/tools/index.js +23 -0
- package/ts_build/src/workers/tools/index.js.map +1 -0
- package/ts_build/src/workers/tools/listAllowedPorts.d.ts +3 -0
- package/ts_build/src/workers/tools/listAllowedPorts.js +25 -0
- package/ts_build/src/workers/tools/listAllowedPorts.js.map +1 -0
- package/ts_build/src/workers/tools/listForwardedPorts.d.ts +10 -0
- package/ts_build/src/workers/tools/listForwardedPorts.js +22 -0
- package/ts_build/src/workers/tools/listForwardedPorts.js.map +1 -0
- package/ts_build/tests/worker/handlers/proxyHandler.test.d.ts +1 -0
- package/ts_build/tests/worker/handlers/proxyHandler.test.js +170 -0
- package/ts_build/tests/worker/handlers/proxyHandler.test.js.map +1 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
42
|
"@aws-sdk/client-s3": "^3.588.0",
|
|
43
|
+
"@tyvm/knowhow-tunnel": "^0.0.1",
|
|
43
44
|
"@google/genai": "^0.14.1",
|
|
44
45
|
"@inquirer/editor": "^4.2.18",
|
|
45
46
|
"@linear/sdk": "^12.0.0",
|
|
@@ -634,11 +634,19 @@ Please continue from where you left off and complete the original request.
|
|
|
634
634
|
|
|
635
635
|
const taskCompleted = new Promise<string>((resolve) => {
|
|
636
636
|
agent.agentEvents.once(agent.eventTypes.done, async (doneMsg) => {
|
|
637
|
-
console.log("
|
|
637
|
+
console.log("🎯 [AgentModule] Task Completed");
|
|
638
638
|
done = true;
|
|
639
639
|
output = doneMsg || "No response from the AI";
|
|
640
640
|
// Update task info
|
|
641
641
|
taskInfo = this.taskRegistry.get(taskId);
|
|
642
|
+
|
|
643
|
+
// Wait for AgentSync to finish before resolving
|
|
644
|
+
if (knowhowTaskId) {
|
|
645
|
+
console.log("🎯 [AgentModule] Waiting for sync finalization...");
|
|
646
|
+
await this.agentSync.waitForFinalization();
|
|
647
|
+
console.log("🎯 [AgentModule] Sync finalization complete");
|
|
648
|
+
}
|
|
649
|
+
|
|
642
650
|
if (taskInfo) {
|
|
643
651
|
taskInfo.status = "completed";
|
|
644
652
|
// Update final cost from agent
|
|
@@ -647,7 +655,9 @@ Please continue from where you left off and complete the original request.
|
|
|
647
655
|
this.updateSession(taskId, agent.getThreads());
|
|
648
656
|
taskInfo.endTime = Date.now();
|
|
649
657
|
}
|
|
658
|
+
|
|
650
659
|
console.log(Marked.parse(output));
|
|
660
|
+
console.log("🎯 [AgentModule] Task Complete");
|
|
651
661
|
resolve(doneMsg);
|
|
652
662
|
});
|
|
653
663
|
});
|
package/src/cli.ts
CHANGED
|
@@ -54,10 +54,11 @@ async function setupServices() {
|
|
|
54
54
|
// Add Mcp service to tool context directly so MCP management tools can access it
|
|
55
55
|
Tools.addContext("Mcp", Mcp);
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
console.log("🔌 Connecting to MCP...");
|
|
58
|
+
await Mcp.connectToConfigured(Tools);
|
|
59
|
+
console.log("Connecting to clients...");
|
|
60
|
+
await Clients.registerConfiguredModels();
|
|
61
|
+
console.log("✓ Services are set up and ready to go!");
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// Utility function to read from stdin
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -125,24 +125,14 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
125
125
|
handleMessageCaching(groupedMessages: MessageParam[]) {
|
|
126
126
|
this.handleClearingCache(groupedMessages);
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (hasTwoUserMesages) {
|
|
137
|
-
// find the last two messages and mark them as ephemeral
|
|
138
|
-
const lastTwoUserMessages = groupedMessages
|
|
139
|
-
.filter((m) => m.role === "user")
|
|
140
|
-
.slice(-2);
|
|
141
|
-
|
|
142
|
-
for (const m of lastTwoUserMessages) {
|
|
143
|
-
if (Array.isArray(m.content)) {
|
|
144
|
-
this.cacheLastContent(m);
|
|
145
|
-
}
|
|
128
|
+
// find the last two messages and mark them as ephemeral
|
|
129
|
+
const lastTwoUserMessages = groupedMessages
|
|
130
|
+
.filter((m) => m.role === "user")
|
|
131
|
+
.slice(-2);
|
|
132
|
+
|
|
133
|
+
for (const m of lastTwoUserMessages) {
|
|
134
|
+
if (Array.isArray(m.content)) {
|
|
135
|
+
this.cacheLastContent(m);
|
|
146
136
|
}
|
|
147
137
|
}
|
|
148
138
|
}
|
|
@@ -185,33 +175,39 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
185
175
|
}
|
|
186
176
|
|
|
187
177
|
// Convert tool message content to appropriate format
|
|
188
|
-
let toolResultContent:
|
|
189
|
-
|
|
178
|
+
let toolResultContent:
|
|
179
|
+
| string
|
|
180
|
+
| (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
|
|
181
|
+
|
|
190
182
|
if (typeof msg.content === "string") {
|
|
191
183
|
toolResultContent = msg.content;
|
|
192
184
|
} else if (Array.isArray(msg.content)) {
|
|
193
185
|
// Transform image_url format to Anthropic's image format
|
|
194
|
-
toolResultContent = msg.content.map(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
186
|
+
toolResultContent = msg.content.map(
|
|
187
|
+
(item): Anthropic.TextBlockParam | Anthropic.ImageBlockParam => {
|
|
188
|
+
if (item.type === "image_url") {
|
|
189
|
+
const url = item.image_url.url;
|
|
190
|
+
const isDataUrl = url.startsWith("data:");
|
|
191
|
+
const base64Data = isDataUrl ? url.split(",")[1] : url;
|
|
192
|
+
const mediaType = isDataUrl
|
|
193
|
+
? url.match(/data:([^;]+);/)?.[1] || "image/jpeg"
|
|
194
|
+
: "image/jpeg";
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
type: "image" as const,
|
|
198
|
+
source: {
|
|
199
|
+
type: "base64" as const,
|
|
200
|
+
media_type: mediaType as any,
|
|
201
|
+
data: base64Data,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
} else if (item.type === "text") {
|
|
205
|
+
return { type: "text" as const, text: item.text };
|
|
206
|
+
}
|
|
207
|
+
// Fallback for unknown types
|
|
208
|
+
return { type: "text" as const, text: String(item) };
|
|
211
209
|
}
|
|
212
|
-
|
|
213
|
-
return { type: "text" as const, text: String(item) };
|
|
214
|
-
}) as (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
|
|
210
|
+
) as (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
|
|
215
211
|
} else {
|
|
216
212
|
toolResultContent = String(msg.content);
|
|
217
213
|
}
|
|
@@ -293,7 +289,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
293
289
|
? [
|
|
294
290
|
{
|
|
295
291
|
text: systemMessage,
|
|
296
|
-
|
|
292
|
+
cache_control: { type: "ephemeral" },
|
|
297
293
|
type: "text",
|
|
298
294
|
},
|
|
299
295
|
]
|
|
@@ -359,9 +355,19 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
359
355
|
return {
|
|
360
356
|
[Models.anthropic.Opus4_6]: {
|
|
361
357
|
input: 5.0,
|
|
358
|
+
input_gt_200k: 10.0,
|
|
362
359
|
cache_write: 6.25,
|
|
363
360
|
cache_hit: 0.5,
|
|
364
361
|
output: 25.0,
|
|
362
|
+
output_gt_200k: 37.5,
|
|
363
|
+
},
|
|
364
|
+
[Models.anthropic.Sonnet4_6]: {
|
|
365
|
+
input: 3.0,
|
|
366
|
+
input_gt_200k: 6.0,
|
|
367
|
+
cache_write: 3.75,
|
|
368
|
+
cache_hit: 0.3,
|
|
369
|
+
output: 15.0,
|
|
370
|
+
output_gt_200k: 22.5,
|
|
365
371
|
},
|
|
366
372
|
[Models.anthropic.Opus4_5]: {
|
|
367
373
|
input: 5.0,
|
|
@@ -383,15 +389,19 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
383
389
|
},
|
|
384
390
|
[Models.anthropic.Sonnet4]: {
|
|
385
391
|
input: 3.0,
|
|
392
|
+
input_gt_200k: 6.0,
|
|
386
393
|
cache_write: 3.75,
|
|
387
394
|
cache_hit: 0.3,
|
|
388
395
|
output: 15.0,
|
|
396
|
+
output_gt_200k: 22.5,
|
|
389
397
|
},
|
|
390
398
|
[Models.anthropic.Sonnet4_5]: {
|
|
391
399
|
input: 3.0,
|
|
400
|
+
input_gt_200k: 6.0,
|
|
392
401
|
cache_write: 3.75,
|
|
393
402
|
cache_hit: 0.3,
|
|
394
403
|
output: 15.0,
|
|
404
|
+
output_gt_200k: 22.5,
|
|
395
405
|
},
|
|
396
406
|
[Models.anthropic.Haiku4_5]: {
|
|
397
407
|
input: 1,
|
|
@@ -413,8 +423,8 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
413
423
|
},
|
|
414
424
|
[Models.anthropic.Haiku3_5]: {
|
|
415
425
|
input: 0.8,
|
|
416
|
-
cache_write: 1.
|
|
417
|
-
cache_hit: 0.
|
|
426
|
+
cache_write: 1.0,
|
|
427
|
+
cache_hit: 0.08,
|
|
418
428
|
output: 4.0,
|
|
419
429
|
},
|
|
420
430
|
[Models.anthropic.Opus3]: {
|
|
@@ -425,34 +435,45 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
425
435
|
},
|
|
426
436
|
[Models.anthropic.Haiku3]: {
|
|
427
437
|
input: 0.25,
|
|
428
|
-
cache_write: 0.
|
|
429
|
-
cache_hit: 0.
|
|
438
|
+
cache_write: 0.3125,
|
|
439
|
+
cache_hit: 0.025,
|
|
430
440
|
output: 1.25,
|
|
431
441
|
},
|
|
432
442
|
};
|
|
433
443
|
}
|
|
434
444
|
|
|
435
445
|
calculateCost(model: string, usage: Usage): number | undefined {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
446
|
+
const p = this.pricesPerMillion()[model];
|
|
447
|
+
if (!p) return undefined;
|
|
448
|
+
|
|
449
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
450
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
451
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
452
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
453
|
+
|
|
454
|
+
const totalInputTokens = inputTokens + cacheWriteTokens + cacheReadTokens;
|
|
455
|
+
|
|
456
|
+
const useLongContextTier = totalInputTokens > 200_000 && !!p.input_gt_200k;
|
|
457
|
+
const inputRate = useLongContextTier
|
|
458
|
+
? (p.input_gt_200k as number)
|
|
459
|
+
: p.input;
|
|
460
|
+
const outputRate =
|
|
461
|
+
useLongContextTier && p.output_gt_200k ? p.output_gt_200k : p.output;
|
|
462
|
+
|
|
463
|
+
// Prefer modeling cache pricing as multipliers, but if you keep absolute numbers,
|
|
464
|
+
// you MUST scale them when usingLongContextTier.
|
|
465
|
+
//
|
|
466
|
+
// Anthropic docs describe cache read/write as multipliers of the base input rate. :contentReference[oaicite:7]{index=7}
|
|
467
|
+
// If your `cache_write` + `cache_hit` are absolute $/MTok at base tier, scale them:
|
|
468
|
+
const cacheWriteRate = (p.cache_write / p.input) * inputRate; // preserves your multiplier
|
|
469
|
+
const cacheReadRate = (p.cache_hit / p.input) * inputRate; // preserves your multiplier
|
|
470
|
+
|
|
471
|
+
const nonCachedInputCost = (inputTokens * inputRate) / 1e6;
|
|
472
|
+
const cacheWriteCost = (cacheWriteTokens * cacheWriteRate) / 1e6;
|
|
473
|
+
const cacheReadCost = (cacheReadTokens * cacheReadRate) / 1e6;
|
|
474
|
+
const outputCost = (outputTokens * outputRate) / 1e6;
|
|
475
|
+
|
|
476
|
+
return nonCachedInputCost + cacheWriteCost + cacheReadCost + outputCost;
|
|
456
477
|
}
|
|
457
478
|
|
|
458
479
|
async getModels() {
|
package/src/config.ts
CHANGED
|
@@ -32,6 +32,7 @@ export class AgentSynchronization {
|
|
|
32
32
|
private baseUrl: string;
|
|
33
33
|
private knowhowTaskId: string | undefined;
|
|
34
34
|
private eventHandlersSetup: boolean = false;
|
|
35
|
+
private finalizationPromise: Promise<void> | null = null;
|
|
35
36
|
|
|
36
37
|
constructor(baseUrl: string = KNOWHOW_API_URL) {
|
|
37
38
|
this.baseUrl = baseUrl;
|
|
@@ -248,28 +249,44 @@ export class AgentSynchronization {
|
|
|
248
249
|
// Listen to completion event to finalize task
|
|
249
250
|
agent.agentEvents.on(agent.eventTypes.done, async (result: string) => {
|
|
250
251
|
if (!this.knowhowTaskId || !this.baseUrl) {
|
|
252
|
+
console.warn(`⚠️ [AgentSync] Cannot finalize: knowhowTaskId=${this.knowhowTaskId}, baseUrl=${this.baseUrl}`);
|
|
251
253
|
return;
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
console.log(`🎯 [AgentSync] Done event received for task: ${this.knowhowTaskId}`);
|
|
257
|
+
|
|
258
|
+
// Create a promise that tracks finalization
|
|
259
|
+
this.finalizationPromise = (async () => {
|
|
260
|
+
try {
|
|
261
|
+
console.log(
|
|
262
|
+
`Updating Knowhow chat task on completion..., ${this.knowhowTaskId}`
|
|
263
|
+
);
|
|
264
|
+
await this.updateChatTask(this.knowhowTaskId!, agent, false, result);
|
|
265
|
+
console.log(`✅ Completed Knowhow chat task: ${this.knowhowTaskId}`);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error(`❌ Error finalizing task:`, error);
|
|
268
|
+
throw error; // Re-throw so CLI can handle it
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
264
271
|
});
|
|
265
272
|
}
|
|
266
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Wait for finalization to complete (for CLI usage)
|
|
276
|
+
*/
|
|
277
|
+
async waitForFinalization(): Promise<void> {
|
|
278
|
+
if (this.finalizationPromise) {
|
|
279
|
+
await this.finalizationPromise;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
267
283
|
/**
|
|
268
284
|
* Reset synchronization state (useful for reusing the service)
|
|
269
285
|
*/
|
|
270
286
|
reset(): void {
|
|
271
287
|
this.knowhowTaskId = undefined;
|
|
272
288
|
this.eventHandlersSetup = false;
|
|
289
|
+
this.finalizationPromise = null;
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
/**
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,16 @@ export type Config = {
|
|
|
66
66
|
sandbox?: boolean;
|
|
67
67
|
volumes?: string[];
|
|
68
68
|
envFile?: string;
|
|
69
|
+
tunnel?: {
|
|
70
|
+
enabled?: boolean;
|
|
71
|
+
allowedPorts?: number[];
|
|
72
|
+
maxConcurrentStreams?: number;
|
|
73
|
+
portMapping?: {
|
|
74
|
+
[containerPort: number]: number; // containerPort -> hostPort
|
|
75
|
+
};
|
|
76
|
+
localHost?: string; // Default: "127.0.0.1", can be "host.docker.internal" for Docker
|
|
77
|
+
enableUrlRewriting?: boolean; // Enable URL rewriting for localhost URLs (default: true)
|
|
78
|
+
};
|
|
69
79
|
};
|
|
70
80
|
};
|
|
71
81
|
|
|
@@ -143,6 +153,7 @@ export type ChatInteraction = {
|
|
|
143
153
|
export const Models = {
|
|
144
154
|
anthropic: {
|
|
145
155
|
Opus4_6: "claude-opus-4-6",
|
|
156
|
+
Sonnet4_6: "claude-sonnet-4-6",
|
|
146
157
|
Opus4_5: "claude-opus-4-5-20251101",
|
|
147
158
|
Opus4: "claude-opus-4-20250514",
|
|
148
159
|
Opus4_1: "claude-opus-4-1-20250805",
|
package/src/worker.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
|
+
import { createTunnelHandler, TunnelHandler } from "@tyvm/knowhow-tunnel";
|
|
3
4
|
import { includedTools } from "./agents/tools/list";
|
|
4
5
|
import { loadJwt } from "./login";
|
|
5
6
|
import { services } from "./services";
|
|
6
7
|
import { McpServerService } from "./services/Mcp";
|
|
7
8
|
import * as allTools from "./agents/tools";
|
|
9
|
+
import workerTools from "./workers/tools";
|
|
8
10
|
import { wait } from "./utils";
|
|
9
11
|
import { getConfig, updateConfig } from "./config";
|
|
10
12
|
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
@@ -88,9 +90,11 @@ export async function worker(options?: {
|
|
|
88
90
|
|
|
89
91
|
// Check if we're already running inside a Docker container
|
|
90
92
|
const isInsideDocker = process.env.KNOWHOW_DOCKER === "true";
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
if (isInsideDocker) {
|
|
93
|
-
console.log(
|
|
95
|
+
console.log(
|
|
96
|
+
"🐳 Already running inside Docker container, skipping sandbox mode"
|
|
97
|
+
);
|
|
94
98
|
// Force sandbox mode off when inside Docker to prevent nested containers
|
|
95
99
|
if (options) {
|
|
96
100
|
options.sandbox = false;
|
|
@@ -145,6 +149,11 @@ export async function worker(options?: {
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
const { Tools } = services();
|
|
152
|
+
// Combine agent tools and worker-specific tools
|
|
153
|
+
const combinedTools = { ...allTools, ...workerTools.tools };
|
|
154
|
+
Tools.defineTools(includedTools, combinedTools);
|
|
155
|
+
Tools.defineTools(workerTools.definitions, workerTools.tools);
|
|
156
|
+
|
|
148
157
|
const mcpServer = new McpServerService(Tools);
|
|
149
158
|
const clientName = "knowhow-worker";
|
|
150
159
|
const clientVersion = "1.1.1";
|
|
@@ -178,6 +187,74 @@ export async function worker(options?: {
|
|
|
178
187
|
mcpServer.createServer(clientName, clientVersion).withTools(toolsToUse);
|
|
179
188
|
|
|
180
189
|
let connected = false;
|
|
190
|
+
let tunnelHandler: TunnelHandler | null = null;
|
|
191
|
+
let tunnelWs: WebSocket | null = null;
|
|
192
|
+
|
|
193
|
+
// Check if tunnel is enabled
|
|
194
|
+
const tunnelEnabled = config.worker?.tunnel?.enabled ?? false;
|
|
195
|
+
|
|
196
|
+
// Determine localHost based on environment
|
|
197
|
+
let tunnelLocalHost = config.worker?.tunnel?.localHost;
|
|
198
|
+
if (!tunnelLocalHost) {
|
|
199
|
+
// Auto-detect based on Docker environment
|
|
200
|
+
if (isInsideDocker) {
|
|
201
|
+
tunnelLocalHost = "host.docker.internal";
|
|
202
|
+
console.log(
|
|
203
|
+
"🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
tunnelLocalHost = "127.0.0.1";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check for port mapping configuration
|
|
211
|
+
const portMapping = config.worker?.tunnel?.portMapping || {};
|
|
212
|
+
if (Object.keys(portMapping).length > 0) {
|
|
213
|
+
console.log("🔀 Port mapping configured:");
|
|
214
|
+
for (const [containerPort, hostPort] of Object.entries(portMapping)) {
|
|
215
|
+
console.log(` Container port ${containerPort} → Host port ${hostPort}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (tunnelEnabled) {
|
|
220
|
+
const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
|
|
221
|
+
if (tunnelPorts.length === 0) {
|
|
222
|
+
console.warn(
|
|
223
|
+
"⚠️ Tunnel enabled but no allowedPorts configured. Add tunnel.allowedPorts to knowhow.json"
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(`🌐 Tunnel enabled for ports: ${tunnelPorts.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
console.log(
|
|
230
|
+
"🚫 Tunnel disabled (enable in knowhow.json: worker.tunnel.enabled = true)"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extract tunnel domain from API_URL
|
|
235
|
+
// e.g., "https://api.knowhow.tyvm.ai" -> "knowhow.tyvm.ai"
|
|
236
|
+
// e.g., "http://localhost:4000" -> "localhost:4000"
|
|
237
|
+
function extractTunnelDomain(apiUrl: string): {
|
|
238
|
+
domain: string;
|
|
239
|
+
useHttps: boolean;
|
|
240
|
+
} {
|
|
241
|
+
try {
|
|
242
|
+
const url = new URL(apiUrl);
|
|
243
|
+
const useHttps = url.protocol === "https:";
|
|
244
|
+
|
|
245
|
+
// For localhost, include port; for production, just use hostname
|
|
246
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
247
|
+
return {
|
|
248
|
+
domain: `worker.${url.hostname}:${url.port || "80"}`,
|
|
249
|
+
useHttps,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { domain: `worker.${url.hostname}`, useHttps };
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error("Failed to parse API_URL for tunnel domain:", err);
|
|
255
|
+
return { domain: "worker.localhost:4000", useHttps: false }; // fallback
|
|
256
|
+
}
|
|
257
|
+
}
|
|
181
258
|
|
|
182
259
|
async function connectWebSocket() {
|
|
183
260
|
const jwt = await loadJwt();
|
|
@@ -185,10 +262,12 @@ export async function worker(options?: {
|
|
|
185
262
|
|
|
186
263
|
const dir = process.cwd();
|
|
187
264
|
const homedir = os.homedir();
|
|
188
|
-
|
|
265
|
+
|
|
189
266
|
// Use environment variables if available (set by Docker), otherwise compute defaults
|
|
190
267
|
const hostname = process.env.WORKER_HOSTNAME || os.hostname();
|
|
191
|
-
const root =
|
|
268
|
+
const root =
|
|
269
|
+
process.env.WORKER_ROOT ||
|
|
270
|
+
(dir === homedir ? "~" : dir.replace(homedir, "~"));
|
|
192
271
|
|
|
193
272
|
const headers: Record<string, string> = {
|
|
194
273
|
Authorization: `Bearer ${jwt}`,
|
|
@@ -207,12 +286,61 @@ export async function worker(options?: {
|
|
|
207
286
|
console.log("🔒 Worker is private (only you can use it)");
|
|
208
287
|
}
|
|
209
288
|
|
|
289
|
+
const { domain: tunnelDomain, useHttps: tunnelUseHttps } =
|
|
290
|
+
extractTunnelDomain(API_URL);
|
|
291
|
+
|
|
210
292
|
const ws = new WebSocket(`${API_URL}/ws/worker`, {
|
|
211
293
|
headers,
|
|
212
294
|
});
|
|
213
295
|
|
|
296
|
+
// Create separate WebSocket connection for tunnel if enabled
|
|
297
|
+
let tunnelConnection: WebSocket | null = null;
|
|
298
|
+
if (tunnelEnabled) {
|
|
299
|
+
tunnelConnection = new WebSocket(`${API_URL}/ws/tunnel`, {
|
|
300
|
+
headers,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
tunnelConnection.on("open", () => {
|
|
304
|
+
console.log("Tunnel WebSocket connected");
|
|
305
|
+
|
|
306
|
+
// Initialize tunnel handler with the tunnel-specific WebSocket
|
|
307
|
+
tunnelHandler = createTunnelHandler(tunnelConnection!, {
|
|
308
|
+
allowedPorts: config.worker?.tunnel?.allowedPorts || [],
|
|
309
|
+
maxConcurrentStreams:
|
|
310
|
+
config.worker?.tunnel?.maxConcurrentStreams || 50,
|
|
311
|
+
localHost: tunnelLocalHost,
|
|
312
|
+
tunnelDomain,
|
|
313
|
+
tunnelUseHttps,
|
|
314
|
+
enableUrlRewriting:
|
|
315
|
+
config.worker?.tunnel?.enableUrlRewriting !== false,
|
|
316
|
+
portMapping,
|
|
317
|
+
logLevel: "info",
|
|
318
|
+
});
|
|
319
|
+
console.log("🌐 Tunnel handler initialized");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
tunnelConnection.on("close", (code, reason) => {
|
|
323
|
+
console.log(
|
|
324
|
+
`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Cleanup tunnel handler
|
|
328
|
+
if (tunnelHandler) {
|
|
329
|
+
tunnelHandler.cleanup();
|
|
330
|
+
tunnelHandler = null;
|
|
331
|
+
}
|
|
332
|
+
tunnelWs = null;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
tunnelConnection.on("error", (error) => {
|
|
336
|
+
console.error("Tunnel WebSocket error:", error);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
tunnelWs = tunnelConnection;
|
|
340
|
+
}
|
|
341
|
+
|
|
214
342
|
ws.on("open", () => {
|
|
215
|
-
console.log("
|
|
343
|
+
console.log("Worker WebSocket connected");
|
|
216
344
|
connected = true;
|
|
217
345
|
});
|
|
218
346
|
|
|
@@ -221,6 +349,12 @@ export async function worker(options?: {
|
|
|
221
349
|
`WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`
|
|
222
350
|
);
|
|
223
351
|
console.log("Attempting to reconnect...");
|
|
352
|
+
|
|
353
|
+
// Cleanup tunnel handler
|
|
354
|
+
if (tunnelHandler) {
|
|
355
|
+
tunnelHandler = null;
|
|
356
|
+
}
|
|
357
|
+
|
|
224
358
|
connected = false;
|
|
225
359
|
});
|
|
226
360
|
|
|
@@ -230,12 +364,15 @@ export async function worker(options?: {
|
|
|
230
364
|
|
|
231
365
|
mcpServer.runWsServer(ws);
|
|
232
366
|
|
|
233
|
-
return { ws, mcpServer };
|
|
367
|
+
return { ws, mcpServer, tunnelWs };
|
|
234
368
|
}
|
|
235
369
|
|
|
236
370
|
while (true) {
|
|
237
|
-
let connection: {
|
|
238
|
-
|
|
371
|
+
let connection: {
|
|
372
|
+
ws: WebSocket;
|
|
373
|
+
mcpServer: McpServerService;
|
|
374
|
+
tunnelWs: WebSocket | null;
|
|
375
|
+
} | null = null;
|
|
239
376
|
|
|
240
377
|
if (!connected) {
|
|
241
378
|
console.log("Attempting to connect...");
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getConfig } from "../../config";
|
|
2
|
+
import { Tool } from "../../clients/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tool to list all forwarded ports configured for the worker tunnel
|
|
6
|
+
* This reads from the worker.tunnel.allowedPorts configuration
|
|
7
|
+
*/
|
|
8
|
+
export async function listAllowedPorts(): Promise<number[]> {
|
|
9
|
+
const config = await getConfig();
|
|
10
|
+
|
|
11
|
+
if (!config.worker?.tunnel?.enabled) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return config.worker.tunnel.allowedPorts || [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const listAllowedPortsDefinition: Tool = {
|
|
19
|
+
type: "function" as const,
|
|
20
|
+
function: {
|
|
21
|
+
name: "listAllowedPorts",
|
|
22
|
+
description:
|
|
23
|
+
"List all ports that are being forwarded through the worker tunnel. Returns an array of port numbers that can be accessed via the tunnel system.",
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {},
|
|
27
|
+
required: [],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
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.66",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
42
|
"@aws-sdk/client-s3": "^3.588.0",
|
|
43
|
+
"@tyvm/knowhow-tunnel": "^0.0.1",
|
|
43
44
|
"@google/genai": "^0.14.1",
|
|
44
45
|
"@inquirer/editor": "^4.2.18",
|
|
45
46
|
"@linear/sdk": "^12.0.0",
|