@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.
Files changed (53) hide show
  1. package/package.json +2 -1
  2. package/src/chat/modules/AgentModule.ts +11 -1
  3. package/src/cli.ts +5 -4
  4. package/src/clients/anthropic.ts +86 -65
  5. package/src/config.ts +7 -0
  6. package/src/services/AgentSynchronization.ts +27 -10
  7. package/src/types.ts +11 -0
  8. package/src/worker.ts +145 -8
  9. package/src/workers/tools/index.ts +10 -0
  10. package/src/workers/tools/listAllowedPorts.ts +30 -0
  11. package/ts_build/package.json +2 -1
  12. package/ts_build/src/chat/modules/AgentModule.js +7 -1
  13. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  14. package/ts_build/src/cli.js +5 -4
  15. package/ts_build/src/cli.js.map +1 -1
  16. package/ts_build/src/clients/anthropic.d.ts +9 -0
  17. package/ts_build/src/clients/anthropic.js +47 -31
  18. package/ts_build/src/clients/anthropic.js.map +1 -1
  19. package/ts_build/src/config.js +6 -0
  20. package/ts_build/src/config.js.map +1 -1
  21. package/ts_build/src/services/AgentSynchronization.d.ts +2 -0
  22. package/ts_build/src/services/AgentSynchronization.js +20 -10
  23. package/ts_build/src/services/AgentSynchronization.js.map +1 -1
  24. package/ts_build/src/types.d.ts +11 -0
  25. package/ts_build/src/types.js +1 -0
  26. package/ts_build/src/types.js.map +1 -1
  27. package/ts_build/src/worker/handlers/proxyHandler.d.ts +2 -0
  28. package/ts_build/src/worker/handlers/proxyHandler.js +41 -0
  29. package/ts_build/src/worker/handlers/proxyHandler.js.map +1 -0
  30. package/ts_build/src/worker/tools/index.d.ts +1 -0
  31. package/ts_build/src/worker/tools/index.js +18 -0
  32. package/ts_build/src/worker/tools/index.js.map +1 -0
  33. package/ts_build/src/worker/tools/portForwarding.d.ts +49 -0
  34. package/ts_build/src/worker/tools/portForwarding.js +173 -0
  35. package/ts_build/src/worker/tools/portForwarding.js.map +1 -0
  36. package/ts_build/src/worker/types/proxy.d.ts +18 -0
  37. package/ts_build/src/worker/types/proxy.js +3 -0
  38. package/ts_build/src/worker/types/proxy.js.map +1 -0
  39. package/ts_build/src/worker.js +119 -3
  40. package/ts_build/src/worker.js.map +1 -1
  41. package/ts_build/src/workers/tools/index.d.ts +9 -0
  42. package/ts_build/src/workers/tools/index.js +23 -0
  43. package/ts_build/src/workers/tools/index.js.map +1 -0
  44. package/ts_build/src/workers/tools/listAllowedPorts.d.ts +3 -0
  45. package/ts_build/src/workers/tools/listAllowedPorts.js +25 -0
  46. package/ts_build/src/workers/tools/listAllowedPorts.js.map +1 -0
  47. package/ts_build/src/workers/tools/listForwardedPorts.d.ts +10 -0
  48. package/ts_build/src/workers/tools/listForwardedPorts.js +22 -0
  49. package/ts_build/src/workers/tools/listForwardedPorts.js.map +1 -0
  50. package/ts_build/tests/worker/handlers/proxyHandler.test.d.ts +1 -0
  51. package/ts_build/tests/worker/handlers/proxyHandler.test.js +170 -0
  52. package/ts_build/tests/worker/handlers/proxyHandler.test.js.map +1 -0
  53. package/tsconfig.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.64",
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("Agent has completed the task.");
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
- await Promise.all([
58
- Mcp.connectToConfigured(Tools),
59
- Clients.registerConfiguredModels(),
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
@@ -125,24 +125,14 @@ export class GenericAnthropicClient implements GenericClient {
125
125
  handleMessageCaching(groupedMessages: MessageParam[]) {
126
126
  this.handleClearingCache(groupedMessages);
127
127
 
128
- const hasTwoUserMesages =
129
- groupedMessages.filter((m) => m.role === "user").length >= 2;
130
-
131
- const firstUserMessage = groupedMessages.find((m) => m.role === "user");
132
- if (firstUserMessage) {
133
- this.cacheLastContent(firstUserMessage);
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: string | (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
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((item): Anthropic.TextBlockParam | Anthropic.ImageBlockParam => {
195
- if (item.type === "image_url") {
196
- const url = item.image_url.url;
197
- const isDataUrl = url.startsWith("data:");
198
- const base64Data = isDataUrl ? url.split(",")[1] : url;
199
- const mediaType = isDataUrl ? url.match(/data:([^;]+);/)?.[1] || "image/jpeg" : "image/jpeg";
200
-
201
- return {
202
- type: "image" as const,
203
- source: {
204
- type: "base64" as const,
205
- media_type: mediaType as any,
206
- data: base64Data,
207
- },
208
- };
209
- } else if (item.type === "text") {
210
- return { type: "text" as const, text: item.text };
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
- // Fallback for unknown types
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
- // cache_control: { type: "ephemeral" },
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.25,
417
- cache_hit: 0.1,
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.3,
429
- cache_hit: 0.03,
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 pricing = this.pricesPerMillion()[model];
437
-
438
- if (!pricing) {
439
- return undefined;
440
- }
441
-
442
- const cachedInputTokens = usage.cache_creation_input_tokens;
443
- const cachedInputCost = (cachedInputTokens * pricing.cache_write) / 1e6;
444
-
445
- const cachedReadTokens = usage.cache_read_input_tokens;
446
- const cachedReadCost = (cachedReadTokens * pricing.cache_hit) / 1e6;
447
-
448
- const inputTokens = usage.input_tokens;
449
- const inputCost = ((inputTokens - cachedInputCost) * pricing.input) / 1e6;
450
-
451
- const outputTokens = usage.output_tokens;
452
- const outputCost = (outputTokens * pricing.output) / 1e6;
453
-
454
- const total = cachedInputCost + inputCost + outputCost;
455
- return total;
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
@@ -87,6 +87,13 @@ const defaultConfig = {
87
87
  logLevel: "info",
88
88
  completionTimeout: 5000,
89
89
  },
90
+
91
+ worker: {
92
+ tunnel: {
93
+ enabled: false,
94
+ allowedPorts: [],
95
+ },
96
+ },
90
97
  } as Config;
91
98
 
92
99
  const defaultLanguage = {
@@ -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
- try {
255
- await wait(200);
256
- console.log(
257
- `Updating Knowhow chat task on completion..., ${this.knowhowTaskId}`
258
- );
259
- await this.updateChatTask(this.knowhowTaskId, agent, false, result);
260
- console.log(`✅ Completed Knowhow chat task: ${this.knowhowTaskId}`);
261
- } catch (error) {
262
- console.error(`❌ Error finalizing task:`, error);
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("🐳 Already running inside Docker container, skipping sandbox mode");
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 = process.env.WORKER_ROOT || (dir === homedir ? "~" : dir.replace(homedir, "~"));
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("Connected to the server");
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: { ws: WebSocket; mcpServer: McpServerService } | null =
238
- null;
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,10 @@
1
+ export * from "./listAllowedPorts";
2
+ import {
3
+ listAllowedPorts,
4
+ listAllowedPortsDefinition,
5
+ } from "./listAllowedPorts";
6
+
7
+ export default {
8
+ tools: { listAllowedPorts },
9
+ definitions: [listAllowedPortsDefinition],
10
+ };
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.64",
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",