@quilltap/plugin-utils 1.4.0 → 1.5.0

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.
@@ -0,0 +1,245 @@
1
+ import { ToolCallRequest, AnthropicToolDefinition, UniversalTool, GoogleToolDefinition, OpenAIToolDefinition } from '@quilltap/plugin-types';
2
+
3
+ /**
4
+ * Tool Call Parsers
5
+ *
6
+ * Provider-specific parsers for extracting tool calls from LLM responses.
7
+ * Each parser converts from a provider's native format to the standardized
8
+ * ToolCallRequest format.
9
+ *
10
+ * @module @quilltap/plugin-utils/tools/parsers
11
+ */
12
+
13
+ /**
14
+ * Supported tool call response formats
15
+ */
16
+ type ToolCallFormat = 'openai' | 'anthropic' | 'google' | 'auto';
17
+ /**
18
+ * Parse OpenAI format tool calls from LLM response
19
+ *
20
+ * Extracts tool calls from OpenAI/Grok API responses which return
21
+ * tool_calls in the message object.
22
+ *
23
+ * Expected response structures:
24
+ * - `response.tool_calls` (direct)
25
+ * - `response.choices[0].message.tool_calls` (nested)
26
+ *
27
+ * @param response - The raw response from provider API
28
+ * @returns Array of parsed tool call requests
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const response = await openai.chat.completions.create({...});
33
+ * const toolCalls = parseOpenAIToolCalls(response);
34
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
35
+ * ```
36
+ */
37
+ declare function parseOpenAIToolCalls(response: unknown): ToolCallRequest[];
38
+ /**
39
+ * Parse Anthropic format tool calls from LLM response
40
+ *
41
+ * Extracts tool calls from Anthropic API responses which return
42
+ * tool_use blocks in the content array.
43
+ *
44
+ * Expected response structure:
45
+ * - `response.content` array with `{ type: 'tool_use', name, input }`
46
+ *
47
+ * @param response - The raw response from provider API
48
+ * @returns Array of parsed tool call requests
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const response = await anthropic.messages.create({...});
53
+ * const toolCalls = parseAnthropicToolCalls(response);
54
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
55
+ * ```
56
+ */
57
+ declare function parseAnthropicToolCalls(response: unknown): ToolCallRequest[];
58
+ /**
59
+ * Parse Google Gemini format tool calls from LLM response
60
+ *
61
+ * Extracts tool calls from Google Gemini API responses which return
62
+ * functionCall objects in the parts array.
63
+ *
64
+ * Expected response structure:
65
+ * - `response.candidates[0].content.parts` array with `{ functionCall: { name, args } }`
66
+ *
67
+ * @param response - The raw response from provider API
68
+ * @returns Array of parsed tool call requests
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const response = await gemini.generateContent({...});
73
+ * const toolCalls = parseGoogleToolCalls(response);
74
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
75
+ * ```
76
+ */
77
+ declare function parseGoogleToolCalls(response: unknown): ToolCallRequest[];
78
+ /**
79
+ * Detect the format of a tool call response
80
+ *
81
+ * Analyzes the response structure to determine which provider format it uses.
82
+ *
83
+ * @param response - The raw response from a provider API
84
+ * @returns The detected format, or null if unrecognized
85
+ */
86
+ declare function detectToolCallFormat(response: unknown): ToolCallFormat | null;
87
+ /**
88
+ * Parse tool calls with auto-detection or explicit format
89
+ *
90
+ * A unified parser that can either auto-detect the response format
91
+ * or use a specified format. This is useful when you're not sure
92
+ * which provider's response you're handling.
93
+ *
94
+ * @param response - The raw response from a provider API
95
+ * @param format - The format to use: 'openai', 'anthropic', 'google', or 'auto'
96
+ * @returns Array of parsed tool call requests
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * // Auto-detect format
101
+ * const toolCalls = parseToolCalls(response, 'auto');
102
+ *
103
+ * // Or specify format explicitly
104
+ * const toolCalls = parseToolCalls(response, 'openai');
105
+ * ```
106
+ */
107
+ declare function parseToolCalls(response: unknown, format?: ToolCallFormat): ToolCallRequest[];
108
+ /**
109
+ * Check if a response contains tool calls
110
+ *
111
+ * Quick check to determine if a response has any tool calls
112
+ * without fully parsing them.
113
+ *
114
+ * @param response - The raw response from a provider API
115
+ * @returns True if the response contains tool calls
116
+ */
117
+ declare function hasToolCalls(response: unknown): boolean;
118
+
119
+ /**
120
+ * Tool Format Converters
121
+ *
122
+ * Utilities for converting between different provider tool formats.
123
+ * The universal format (OpenAI-style) serves as the baseline for all conversions.
124
+ *
125
+ * @module @quilltap/plugin-utils/tools/converters
126
+ */
127
+
128
+ /**
129
+ * Convert OpenAI/Universal format tool to Anthropic format
130
+ *
131
+ * Anthropic uses a tool_use format with:
132
+ * - name: string
133
+ * - description: string
134
+ * - input_schema: JSON schema object
135
+ *
136
+ * @param tool - Universal tool or OpenAI tool definition
137
+ * @returns Tool formatted for Anthropic's tool_use
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const anthropicTool = convertToAnthropicFormat(universalTool);
142
+ * // Returns: {
143
+ * // name: 'search_web',
144
+ * // description: 'Search the web',
145
+ * // input_schema: {
146
+ * // type: 'object',
147
+ * // properties: { query: { type: 'string' } },
148
+ * // required: ['query']
149
+ * // }
150
+ * // }
151
+ * ```
152
+ */
153
+ declare function convertToAnthropicFormat(tool: UniversalTool | OpenAIToolDefinition): AnthropicToolDefinition;
154
+ /**
155
+ * Convert OpenAI/Universal format tool to Google Gemini format
156
+ *
157
+ * Google uses a function calling format with:
158
+ * - name: string
159
+ * - description: string
160
+ * - parameters: JSON schema object
161
+ *
162
+ * @param tool - Universal tool or OpenAI tool definition
163
+ * @returns Tool formatted for Google's functionCall
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const googleTool = convertToGoogleFormat(universalTool);
168
+ * // Returns: {
169
+ * // name: 'search_web',
170
+ * // description: 'Search the web',
171
+ * // parameters: {
172
+ * // type: 'object',
173
+ * // properties: { query: { type: 'string' } },
174
+ * // required: ['query']
175
+ * // }
176
+ * // }
177
+ * ```
178
+ */
179
+ declare function convertToGoogleFormat(tool: UniversalTool | OpenAIToolDefinition): GoogleToolDefinition;
180
+ /**
181
+ * Convert Anthropic format tool to Universal/OpenAI format
182
+ *
183
+ * @param tool - Anthropic format tool
184
+ * @returns Tool in universal OpenAI format
185
+ */
186
+ declare function convertFromAnthropicFormat(tool: AnthropicToolDefinition): UniversalTool;
187
+ /**
188
+ * Convert Google format tool to Universal/OpenAI format
189
+ *
190
+ * @param tool - Google format tool
191
+ * @returns Tool in universal OpenAI format
192
+ */
193
+ declare function convertFromGoogleFormat(tool: GoogleToolDefinition): UniversalTool;
194
+ /**
195
+ * Apply prompt/description length limit to a tool
196
+ *
197
+ * Modifies a tool's description if it exceeds maxBytes, appending a warning
198
+ * that the description was truncated. This is useful for providers with
199
+ * strict token limits.
200
+ *
201
+ * @param tool - Tool object (any format) with a description property
202
+ * @param maxBytes - Maximum bytes allowed for description (including warning)
203
+ * @returns Modified tool with truncated description if needed
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * const limitedTool = applyDescriptionLimit(tool, 500);
208
+ * // If description > 500 bytes, truncates and adds warning
209
+ * ```
210
+ */
211
+ declare function applyDescriptionLimit<T extends {
212
+ description: string;
213
+ }>(tool: T, maxBytes: number): T;
214
+ /**
215
+ * Target format for tool conversion
216
+ */
217
+ type ToolConvertTarget = 'openai' | 'anthropic' | 'google';
218
+ /**
219
+ * Convert a universal tool to a specific provider format
220
+ *
221
+ * @param tool - Universal tool or OpenAI tool definition
222
+ * @param target - Target provider format
223
+ * @returns Tool in the target format
224
+ */
225
+ declare function convertToolTo(tool: UniversalTool | OpenAIToolDefinition, target: ToolConvertTarget): UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition;
226
+ /**
227
+ * Convert multiple tools to a specific provider format
228
+ *
229
+ * @param tools - Array of universal tools or OpenAI tool definitions
230
+ * @param target - Target provider format
231
+ * @returns Array of tools in the target format
232
+ */
233
+ declare function convertToolsTo(tools: Array<UniversalTool | OpenAIToolDefinition>, target: ToolConvertTarget): Array<UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition>;
234
+ /**
235
+ * Alias for convertToAnthropicFormat
236
+ * @deprecated Use convertToAnthropicFormat instead
237
+ */
238
+ declare const convertOpenAIToAnthropicFormat: typeof convertToAnthropicFormat;
239
+ /**
240
+ * Alias for convertToGoogleFormat
241
+ * @deprecated Use convertToGoogleFormat instead
242
+ */
243
+ declare const convertOpenAIToGoogleFormat: typeof convertToGoogleFormat;
244
+
245
+ export { type ToolCallFormat as T, type ToolConvertTarget as a, applyDescriptionLimit as b, convertFromAnthropicFormat as c, convertFromGoogleFormat as d, convertOpenAIToAnthropicFormat as e, convertOpenAIToGoogleFormat as f, convertToAnthropicFormat as g, convertToGoogleFormat as h, convertToolTo as i, convertToolsTo as j, detectToolCallFormat as k, hasToolCalls as l, parseGoogleToolCalls as m, parseOpenAIToolCalls as n, parseToolCalls as o, parseAnthropicToolCalls as p };
@@ -0,0 +1,245 @@
1
+ import { ToolCallRequest, AnthropicToolDefinition, UniversalTool, GoogleToolDefinition, OpenAIToolDefinition } from '@quilltap/plugin-types';
2
+
3
+ /**
4
+ * Tool Call Parsers
5
+ *
6
+ * Provider-specific parsers for extracting tool calls from LLM responses.
7
+ * Each parser converts from a provider's native format to the standardized
8
+ * ToolCallRequest format.
9
+ *
10
+ * @module @quilltap/plugin-utils/tools/parsers
11
+ */
12
+
13
+ /**
14
+ * Supported tool call response formats
15
+ */
16
+ type ToolCallFormat = 'openai' | 'anthropic' | 'google' | 'auto';
17
+ /**
18
+ * Parse OpenAI format tool calls from LLM response
19
+ *
20
+ * Extracts tool calls from OpenAI/Grok API responses which return
21
+ * tool_calls in the message object.
22
+ *
23
+ * Expected response structures:
24
+ * - `response.tool_calls` (direct)
25
+ * - `response.choices[0].message.tool_calls` (nested)
26
+ *
27
+ * @param response - The raw response from provider API
28
+ * @returns Array of parsed tool call requests
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const response = await openai.chat.completions.create({...});
33
+ * const toolCalls = parseOpenAIToolCalls(response);
34
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
35
+ * ```
36
+ */
37
+ declare function parseOpenAIToolCalls(response: unknown): ToolCallRequest[];
38
+ /**
39
+ * Parse Anthropic format tool calls from LLM response
40
+ *
41
+ * Extracts tool calls from Anthropic API responses which return
42
+ * tool_use blocks in the content array.
43
+ *
44
+ * Expected response structure:
45
+ * - `response.content` array with `{ type: 'tool_use', name, input }`
46
+ *
47
+ * @param response - The raw response from provider API
48
+ * @returns Array of parsed tool call requests
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const response = await anthropic.messages.create({...});
53
+ * const toolCalls = parseAnthropicToolCalls(response);
54
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
55
+ * ```
56
+ */
57
+ declare function parseAnthropicToolCalls(response: unknown): ToolCallRequest[];
58
+ /**
59
+ * Parse Google Gemini format tool calls from LLM response
60
+ *
61
+ * Extracts tool calls from Google Gemini API responses which return
62
+ * functionCall objects in the parts array.
63
+ *
64
+ * Expected response structure:
65
+ * - `response.candidates[0].content.parts` array with `{ functionCall: { name, args } }`
66
+ *
67
+ * @param response - The raw response from provider API
68
+ * @returns Array of parsed tool call requests
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const response = await gemini.generateContent({...});
73
+ * const toolCalls = parseGoogleToolCalls(response);
74
+ * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]
75
+ * ```
76
+ */
77
+ declare function parseGoogleToolCalls(response: unknown): ToolCallRequest[];
78
+ /**
79
+ * Detect the format of a tool call response
80
+ *
81
+ * Analyzes the response structure to determine which provider format it uses.
82
+ *
83
+ * @param response - The raw response from a provider API
84
+ * @returns The detected format, or null if unrecognized
85
+ */
86
+ declare function detectToolCallFormat(response: unknown): ToolCallFormat | null;
87
+ /**
88
+ * Parse tool calls with auto-detection or explicit format
89
+ *
90
+ * A unified parser that can either auto-detect the response format
91
+ * or use a specified format. This is useful when you're not sure
92
+ * which provider's response you're handling.
93
+ *
94
+ * @param response - The raw response from a provider API
95
+ * @param format - The format to use: 'openai', 'anthropic', 'google', or 'auto'
96
+ * @returns Array of parsed tool call requests
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * // Auto-detect format
101
+ * const toolCalls = parseToolCalls(response, 'auto');
102
+ *
103
+ * // Or specify format explicitly
104
+ * const toolCalls = parseToolCalls(response, 'openai');
105
+ * ```
106
+ */
107
+ declare function parseToolCalls(response: unknown, format?: ToolCallFormat): ToolCallRequest[];
108
+ /**
109
+ * Check if a response contains tool calls
110
+ *
111
+ * Quick check to determine if a response has any tool calls
112
+ * without fully parsing them.
113
+ *
114
+ * @param response - The raw response from a provider API
115
+ * @returns True if the response contains tool calls
116
+ */
117
+ declare function hasToolCalls(response: unknown): boolean;
118
+
119
+ /**
120
+ * Tool Format Converters
121
+ *
122
+ * Utilities for converting between different provider tool formats.
123
+ * The universal format (OpenAI-style) serves as the baseline for all conversions.
124
+ *
125
+ * @module @quilltap/plugin-utils/tools/converters
126
+ */
127
+
128
+ /**
129
+ * Convert OpenAI/Universal format tool to Anthropic format
130
+ *
131
+ * Anthropic uses a tool_use format with:
132
+ * - name: string
133
+ * - description: string
134
+ * - input_schema: JSON schema object
135
+ *
136
+ * @param tool - Universal tool or OpenAI tool definition
137
+ * @returns Tool formatted for Anthropic's tool_use
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const anthropicTool = convertToAnthropicFormat(universalTool);
142
+ * // Returns: {
143
+ * // name: 'search_web',
144
+ * // description: 'Search the web',
145
+ * // input_schema: {
146
+ * // type: 'object',
147
+ * // properties: { query: { type: 'string' } },
148
+ * // required: ['query']
149
+ * // }
150
+ * // }
151
+ * ```
152
+ */
153
+ declare function convertToAnthropicFormat(tool: UniversalTool | OpenAIToolDefinition): AnthropicToolDefinition;
154
+ /**
155
+ * Convert OpenAI/Universal format tool to Google Gemini format
156
+ *
157
+ * Google uses a function calling format with:
158
+ * - name: string
159
+ * - description: string
160
+ * - parameters: JSON schema object
161
+ *
162
+ * @param tool - Universal tool or OpenAI tool definition
163
+ * @returns Tool formatted for Google's functionCall
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const googleTool = convertToGoogleFormat(universalTool);
168
+ * // Returns: {
169
+ * // name: 'search_web',
170
+ * // description: 'Search the web',
171
+ * // parameters: {
172
+ * // type: 'object',
173
+ * // properties: { query: { type: 'string' } },
174
+ * // required: ['query']
175
+ * // }
176
+ * // }
177
+ * ```
178
+ */
179
+ declare function convertToGoogleFormat(tool: UniversalTool | OpenAIToolDefinition): GoogleToolDefinition;
180
+ /**
181
+ * Convert Anthropic format tool to Universal/OpenAI format
182
+ *
183
+ * @param tool - Anthropic format tool
184
+ * @returns Tool in universal OpenAI format
185
+ */
186
+ declare function convertFromAnthropicFormat(tool: AnthropicToolDefinition): UniversalTool;
187
+ /**
188
+ * Convert Google format tool to Universal/OpenAI format
189
+ *
190
+ * @param tool - Google format tool
191
+ * @returns Tool in universal OpenAI format
192
+ */
193
+ declare function convertFromGoogleFormat(tool: GoogleToolDefinition): UniversalTool;
194
+ /**
195
+ * Apply prompt/description length limit to a tool
196
+ *
197
+ * Modifies a tool's description if it exceeds maxBytes, appending a warning
198
+ * that the description was truncated. This is useful for providers with
199
+ * strict token limits.
200
+ *
201
+ * @param tool - Tool object (any format) with a description property
202
+ * @param maxBytes - Maximum bytes allowed for description (including warning)
203
+ * @returns Modified tool with truncated description if needed
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * const limitedTool = applyDescriptionLimit(tool, 500);
208
+ * // If description > 500 bytes, truncates and adds warning
209
+ * ```
210
+ */
211
+ declare function applyDescriptionLimit<T extends {
212
+ description: string;
213
+ }>(tool: T, maxBytes: number): T;
214
+ /**
215
+ * Target format for tool conversion
216
+ */
217
+ type ToolConvertTarget = 'openai' | 'anthropic' | 'google';
218
+ /**
219
+ * Convert a universal tool to a specific provider format
220
+ *
221
+ * @param tool - Universal tool or OpenAI tool definition
222
+ * @param target - Target provider format
223
+ * @returns Tool in the target format
224
+ */
225
+ declare function convertToolTo(tool: UniversalTool | OpenAIToolDefinition, target: ToolConvertTarget): UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition;
226
+ /**
227
+ * Convert multiple tools to a specific provider format
228
+ *
229
+ * @param tools - Array of universal tools or OpenAI tool definitions
230
+ * @param target - Target provider format
231
+ * @returns Array of tools in the target format
232
+ */
233
+ declare function convertToolsTo(tools: Array<UniversalTool | OpenAIToolDefinition>, target: ToolConvertTarget): Array<UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition>;
234
+ /**
235
+ * Alias for convertToAnthropicFormat
236
+ * @deprecated Use convertToAnthropicFormat instead
237
+ */
238
+ declare const convertOpenAIToAnthropicFormat: typeof convertToAnthropicFormat;
239
+ /**
240
+ * Alias for convertToGoogleFormat
241
+ * @deprecated Use convertToGoogleFormat instead
242
+ */
243
+ declare const convertOpenAIToGoogleFormat: typeof convertToGoogleFormat;
244
+
245
+ export { type ToolCallFormat as T, type ToolConvertTarget as a, applyDescriptionLimit as b, convertFromAnthropicFormat as c, convertFromGoogleFormat as d, convertOpenAIToAnthropicFormat as e, convertOpenAIToGoogleFormat as f, convertToAnthropicFormat as g, convertToGoogleFormat as h, convertToolTo as i, convertToolsTo as j, detectToolCallFormat as k, hasToolCalls as l, parseGoogleToolCalls as m, parseOpenAIToolCalls as n, parseToolCalls as o, parseAnthropicToolCalls as p };
@@ -13,7 +13,9 @@
13
13
  * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP
14
14
  * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`
15
15
  * (Docker Desktop DNS or --add-host on Linux handles the forwarding)
16
- * 3. Default gateway from /proc/net/route (works in Lima and WSL2 where
16
+ * 2.5. In WSL2: nameserver IP from /etc/resolv.conf (WSL2 auto-generates
17
+ * this to point at the Windows host with special localhost forwarding)
18
+ * 3. Default gateway from /proc/net/route (works in Lima where
17
19
  * NAT networking forwards to host loopback)
18
20
  * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts
19
21
  * 5. Give up gracefully — return URL unchanged
@@ -13,7 +13,9 @@
13
13
  * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP
14
14
  * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`
15
15
  * (Docker Desktop DNS or --add-host on Linux handles the forwarding)
16
- * 3. Default gateway from /proc/net/route (works in Lima and WSL2 where
16
+ * 2.5. In WSL2: nameserver IP from /etc/resolv.conf (WSL2 auto-generates
17
+ * this to point at the Windows host with special localhost forwarding)
18
+ * 3. Default gateway from /proc/net/route (works in Lima where
17
19
  * NAT networking forwards to host loopback)
18
20
  * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts
19
21
  * 5. Give up gracefully — return URL unchanged
@@ -131,6 +131,24 @@ function resolveHostGateway() {
131
131
  cachedGatewayHost = "host.docker.internal";
132
132
  return cachedGatewayHost;
133
133
  }
134
+ try {
135
+ if ((0, import_node_fs.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop")) {
136
+ const resolv = (0, import_node_fs.readFileSync)("/etc/resolv.conf", "utf-8");
137
+ for (const line of resolv.split("\n")) {
138
+ const trimmed = line.trim();
139
+ if (trimmed.startsWith("#") || trimmed === "") continue;
140
+ const match = trimmed.match(/^nameserver\s+(\S+)/);
141
+ if (match) {
142
+ const ip = match[1];
143
+ rewriteLogger.info("WSL2 host IP from /etc/resolv.conf nameserver", { ip });
144
+ cachedGatewayHost = ip;
145
+ return cachedGatewayHost;
146
+ }
147
+ }
148
+ }
149
+ } catch {
150
+ rewriteLogger.debug("Could not detect WSL2 or read /etc/resolv.conf");
151
+ }
134
152
  try {
135
153
  const routeTable = (0, import_node_fs.readFileSync)("/proc/net/route", "utf-8");
136
154
  for (const line of routeTable.split("\n").slice(1)) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/host-rewrite.ts","../src/logging/plugin-logger.ts","../src/logging/index.ts"],"sourcesContent":["/**\n * Host URL Rewriting for VM/Container Environments\n *\n * When Quilltap runs inside Docker, Lima VMs, or WSL2, `localhost` and\n * `127.0.0.1` resolve to the container/VM's own loopback — not the host\n * machine where services like Ollama or LM Studio are running.\n *\n * This module provides a single function that transparently rewrites\n * localhost URLs to point at the host, so users can configure\n * `http://localhost:11434` and have it Just Work in every environment.\n *\n * Gateway resolution order:\n * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP\n * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 3. Default gateway from /proc/net/route (works in Lima and WSL2 where\n * NAT networking forwards to host loopback)\n * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts\n * 5. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { readFileSync, existsSync, statSync } from 'node:fs';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Hostnames that refer to the local loopback */\nconst LOCALHOST_HOSTS = new Set([\n 'localhost',\n '127.0.0.1',\n '[::1]',\n '::1',\n]);\n\n// ============================================================================\n// Cached Gateway Host\n// ============================================================================\n\nlet cachedGatewayHost: string | null | undefined; // undefined = not yet resolved\n\nconst rewriteLogger = createPluginLogger('host-rewrite');\n\n// ============================================================================\n// Environment Detection (self-contained)\n// ============================================================================\n\n/**\n * Check if running inside a Lima VM.\n *\n * Lima VMs are provisioned with LIMA_CONTAINER=true in /etc/profile.d/quilltap.sh.\n */\nfunction isLimaEnvironment(): boolean {\n return process.env.LIMA_CONTAINER === 'true';\n}\n\n/**\n * Check if running in a Docker container.\n *\n * Detects Docker by checking:\n * 1. DOCKER_CONTAINER environment variable\n * 2. Existence of /.dockerenv file\n * 3. Existence of /app directory (Quilltap Docker convention)\n */\nfunction isDockerEnvironment(): boolean {\n if (process.env.DOCKER_CONTAINER === 'true') {\n return true;\n }\n\n // Check for Docker-specific markers\n try {\n if (existsSync('/.dockerenv')) {\n return true;\n }\n // Check for /app as a directory (Quilltap Docker convention)\n const appStat = statSync('/app');\n if (appStat.isDirectory()) {\n return true;\n }\n } catch {\n // Not in Docker\n }\n\n return false;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Check if running in any VM or container environment that needs URL rewriting.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || isLimaEnvironment();\n}\n\n/**\n * Resolve the host gateway address (IP or hostname).\n *\n * Tries multiple strategies in order; caches the result so file reads\n * only happen once per process lifetime.\n */\nexport function resolveHostGateway(): string | null {\n // Return cached result if already resolved\n if (cachedGatewayHost !== undefined) {\n return cachedGatewayHost;\n }\n\n // Strategy 1: Explicit env var override\n const envIP = process.env.QUILLTAP_HOST_IP;\n if (envIP) {\n rewriteLogger.info('Host gateway from QUILLTAP_HOST_IP', { host: envIP });\n cachedGatewayHost = envIP;\n return cachedGatewayHost;\n }\n\n // Strategy 2: Docker — use host.docker.internal directly as a hostname\n // Docker Desktop (macOS/Windows) provides built-in DNS resolution for\n // host.docker.internal via its DNS server (127.0.0.11). Linux Docker\n // needs --add-host=host.docker.internal:host-gateway (handled by the\n // start scripts). Either way, host.docker.internal correctly forwards\n // to services bound to the host's loopback (127.0.0.1).\n //\n // This MUST run before /proc/net/route for Docker because the bridge\n // gateway IP returned by /proc/net/route (e.g. 172.17.0.1) is just\n // the Docker bridge interface — services listening on the host's\n // localhost are NOT reachable through it.\n //\n // Lima VMs are excluded here: they trigger isDockerEnvironment() (they\n // have /app) but don't have host.docker.internal in DNS. Lima uses\n // VZ NAT networking where the /proc/net/route gateway genuinely\n // forwards to the host loopback.\n if (isDockerEnvironment() && !isLimaEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\n }\n\n // Strategy 3: Default gateway from /proc/net/route (Lima/WSL2)\n // Parse the kernel routing table directly instead of shelling out to\n // `ip route`, which is unavailable in Alpine Linux images.\n // The file format is tab-separated with hex-encoded IPs.\n // This works for Lima and WSL2 where NAT networking forwards traffic\n // from the gateway IP to the host's loopback.\n try {\n const routeTable = readFileSync('/proc/net/route', 'utf-8');\n for (const line of routeTable.split('\\n').slice(1)) { // skip header\n const fields = line.trim().split('\\t');\n // fields[1] = Destination, fields[2] = Gateway\n // Default route has destination 00000000\n if (fields.length >= 3 && fields[1] === '00000000') {\n const hexGateway = fields[2];\n // Convert hex gateway to dotted-quad IP (little-endian on Linux)\n const ip = [\n parseInt(hexGateway.substring(6, 8), 16),\n parseInt(hexGateway.substring(4, 6), 16),\n parseInt(hexGateway.substring(2, 4), 16),\n parseInt(hexGateway.substring(0, 2), 16),\n ].join('.');\n rewriteLogger.info('Host gateway IP from /proc/net/route', { ip });\n cachedGatewayHost = ip;\n return cachedGatewayHost;\n }\n }\n } catch {\n rewriteLogger.debug('Could not read /proc/net/route for default gateway lookup');\n }\n\n // Strategy 4: Fallback — resolve host.docker.internal from /etc/hosts\n // Covers edge cases where Docker adds it to /etc/hosts but we're not\n // detected as Docker (e.g., custom container runtimes).\n try {\n const hosts = readFileSync('/etc/hosts', 'utf-8');\n for (const line of hosts.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith('#') || trimmed === '') continue;\n // /etc/hosts format: <IP> <hostname1> [hostname2] ...\n const parts = trimmed.split(/\\s+/);\n if (parts.length >= 2 && parts.slice(1).includes('host.docker.internal')) {\n const ip = parts[0];\n rewriteLogger.info('Host gateway IP from /etc/hosts (host.docker.internal)', { ip });\n cachedGatewayHost = ip;\n return cachedGatewayHost;\n }\n }\n } catch {\n rewriteLogger.debug('Could not read /etc/hosts for host.docker.internal lookup');\n }\n\n rewriteLogger.warn('Could not resolve host gateway — localhost URLs will not be rewritten');\n cachedGatewayHost = null;\n return cachedGatewayHost;\n}\n\n/**\n * Rewrite a localhost URL to point at the host gateway.\n *\n * No-ops when:\n * - Not running in a VM/container environment\n * - The URL doesn't point to localhost or 127.0.0.1\n * - Gateway resolution fails\n *\n * @param url The URL to potentially rewrite\n * @returns The original URL or a rewritten version with the gateway host\n */\nexport function rewriteLocalhostUrl(url: string): string {\n // No-op on bare metal\n if (!isVMEnvironment()) {\n return url;\n }\n\n // Parse the URL to check the hostname\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid URL — return unchanged\n return url;\n }\n\n // Check if hostname is a localhost variant\n if (!LOCALHOST_HOSTS.has(parsed.hostname)) {\n return url;\n }\n\n // Resolve the gateway host\n const gatewayHost = resolveHostGateway();\n if (!gatewayHost) {\n return url;\n }\n\n // Rewrite the hostname\n parsed.hostname = gatewayHost;\n const rewritten = parsed.toString();\n\n rewriteLogger.debug('Rewrote localhost URL', {\n original: url,\n rewritten,\n gatewayHost,\n });\n\n return rewritten;\n}\n\n/**\n * Reset the cached gateway host (for testing).\n * @internal\n */\nexport function _resetGatewayCache(): void {\n cachedGatewayHost = undefined;\n}\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/host-rewrite.ts","../src/logging/plugin-logger.ts","../src/logging/index.ts"],"sourcesContent":["/**\n * Host URL Rewriting for VM/Container Environments\n *\n * When Quilltap runs inside Docker, Lima VMs, or WSL2, `localhost` and\n * `127.0.0.1` resolve to the container/VM's own loopback — not the host\n * machine where services like Ollama or LM Studio are running.\n *\n * This module provides a single function that transparently rewrites\n * localhost URLs to point at the host, so users can configure\n * `http://localhost:11434` and have it Just Work in every environment.\n *\n * Gateway resolution order:\n * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP\n * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 2.5. In WSL2: nameserver IP from /etc/resolv.conf (WSL2 auto-generates\n * this to point at the Windows host with special localhost forwarding)\n * 3. Default gateway from /proc/net/route (works in Lima where\n * NAT networking forwards to host loopback)\n * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts\n * 5. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { readFileSync, existsSync, statSync } from 'node:fs';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Hostnames that refer to the local loopback */\nconst LOCALHOST_HOSTS = new Set([\n 'localhost',\n '127.0.0.1',\n '[::1]',\n '::1',\n]);\n\n// ============================================================================\n// Cached Gateway Host\n// ============================================================================\n\nlet cachedGatewayHost: string | null | undefined; // undefined = not yet resolved\n\nconst rewriteLogger = createPluginLogger('host-rewrite');\n\n// ============================================================================\n// Environment Detection (self-contained)\n// ============================================================================\n\n/**\n * Check if running inside a Lima VM.\n *\n * Lima VMs are provisioned with LIMA_CONTAINER=true in /etc/profile.d/quilltap.sh.\n */\nfunction isLimaEnvironment(): boolean {\n return process.env.LIMA_CONTAINER === 'true';\n}\n\n/**\n * Check if running in a Docker container.\n *\n * Detects Docker by checking:\n * 1. DOCKER_CONTAINER environment variable\n * 2. Existence of /.dockerenv file\n * 3. Existence of /app directory (Quilltap Docker convention)\n */\nfunction isDockerEnvironment(): boolean {\n if (process.env.DOCKER_CONTAINER === 'true') {\n return true;\n }\n\n // Check for Docker-specific markers\n try {\n if (existsSync('/.dockerenv')) {\n return true;\n }\n // Check for /app as a directory (Quilltap Docker convention)\n const appStat = statSync('/app');\n if (appStat.isDirectory()) {\n return true;\n }\n } catch {\n // Not in Docker\n }\n\n return false;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Check if running in any VM or container environment that needs URL rewriting.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || isLimaEnvironment();\n}\n\n/**\n * Resolve the host gateway address (IP or hostname).\n *\n * Tries multiple strategies in order; caches the result so file reads\n * only happen once per process lifetime.\n */\nexport function resolveHostGateway(): string | null {\n // Return cached result if already resolved\n if (cachedGatewayHost !== undefined) {\n return cachedGatewayHost;\n }\n\n // Strategy 1: Explicit env var override\n const envIP = process.env.QUILLTAP_HOST_IP;\n if (envIP) {\n rewriteLogger.info('Host gateway from QUILLTAP_HOST_IP', { host: envIP });\n cachedGatewayHost = envIP;\n return cachedGatewayHost;\n }\n\n // Strategy 2: Docker — use host.docker.internal directly as a hostname\n // Docker Desktop (macOS/Windows) provides built-in DNS resolution for\n // host.docker.internal via its DNS server (127.0.0.11). Linux Docker\n // needs --add-host=host.docker.internal:host-gateway (handled by the\n // start scripts). Either way, host.docker.internal correctly forwards\n // to services bound to the host's loopback (127.0.0.1).\n //\n // This MUST run before /proc/net/route for Docker because the bridge\n // gateway IP returned by /proc/net/route (e.g. 172.17.0.1) is just\n // the Docker bridge interface — services listening on the host's\n // localhost are NOT reachable through it.\n //\n // Lima VMs are excluded here: they trigger isDockerEnvironment() (they\n // have /app) but don't have host.docker.internal in DNS. Lima uses\n // VZ NAT networking where the /proc/net/route gateway genuinely\n // forwards to the host loopback.\n if (isDockerEnvironment() && !isLimaEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\n }\n\n // Strategy 2.5: WSL2 — use nameserver from /etc/resolv.conf\n // WSL2 auto-generates resolv.conf with a nameserver entry pointing to\n // the Windows host's IP on the Hyper-V virtual network. This IP has\n // special forwarding in WSL2's networking stack that can reach services\n // bound to 127.0.0.1 on the Windows host — unlike the /proc/net/route\n // gateway which only reaches the Hyper-V vNIC interface.\n //\n // Detection: /proc/sys/fs/binfmt_misc/WSLInterop exists only in WSL2.\n try {\n if (existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) {\n const resolv = readFileSync('/etc/resolv.conf', 'utf-8');\n for (const line of resolv.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith('#') || trimmed === '') continue;\n const match = trimmed.match(/^nameserver\\s+(\\S+)/);\n if (match) {\n const ip = match[1];\n rewriteLogger.info('WSL2 host IP from /etc/resolv.conf nameserver', { ip });\n cachedGatewayHost = ip;\n return cachedGatewayHost;\n }\n }\n }\n } catch {\n rewriteLogger.debug('Could not detect WSL2 or read /etc/resolv.conf');\n }\n\n // Strategy 3: Default gateway from /proc/net/route (Lima)\n // Parse the kernel routing table directly instead of shelling out to\n // `ip route`, which is unavailable in Alpine Linux images.\n // The file format is tab-separated with hex-encoded IPs.\n // This works for Lima where NAT networking forwards traffic\n // from the gateway IP to the host's loopback.\n try {\n const routeTable = readFileSync('/proc/net/route', 'utf-8');\n for (const line of routeTable.split('\\n').slice(1)) { // skip header\n const fields = line.trim().split('\\t');\n // fields[1] = Destination, fields[2] = Gateway\n // Default route has destination 00000000\n if (fields.length >= 3 && fields[1] === '00000000') {\n const hexGateway = fields[2];\n // Convert hex gateway to dotted-quad IP (little-endian on Linux)\n const ip = [\n parseInt(hexGateway.substring(6, 8), 16),\n parseInt(hexGateway.substring(4, 6), 16),\n parseInt(hexGateway.substring(2, 4), 16),\n parseInt(hexGateway.substring(0, 2), 16),\n ].join('.');\n rewriteLogger.info('Host gateway IP from /proc/net/route', { ip });\n cachedGatewayHost = ip;\n return cachedGatewayHost;\n }\n }\n } catch {\n rewriteLogger.debug('Could not read /proc/net/route for default gateway lookup');\n }\n\n // Strategy 4: Fallback — resolve host.docker.internal from /etc/hosts\n // Covers edge cases where Docker adds it to /etc/hosts but we're not\n // detected as Docker (e.g., custom container runtimes).\n try {\n const hosts = readFileSync('/etc/hosts', 'utf-8');\n for (const line of hosts.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith('#') || trimmed === '') continue;\n // /etc/hosts format: <IP> <hostname1> [hostname2] ...\n const parts = trimmed.split(/\\s+/);\n if (parts.length >= 2 && parts.slice(1).includes('host.docker.internal')) {\n const ip = parts[0];\n rewriteLogger.info('Host gateway IP from /etc/hosts (host.docker.internal)', { ip });\n cachedGatewayHost = ip;\n return cachedGatewayHost;\n }\n }\n } catch {\n rewriteLogger.debug('Could not read /etc/hosts for host.docker.internal lookup');\n }\n\n rewriteLogger.warn('Could not resolve host gateway — localhost URLs will not be rewritten');\n cachedGatewayHost = null;\n return cachedGatewayHost;\n}\n\n/**\n * Rewrite a localhost URL to point at the host gateway.\n *\n * No-ops when:\n * - Not running in a VM/container environment\n * - The URL doesn't point to localhost or 127.0.0.1\n * - Gateway resolution fails\n *\n * @param url The URL to potentially rewrite\n * @returns The original URL or a rewritten version with the gateway host\n */\nexport function rewriteLocalhostUrl(url: string): string {\n // No-op on bare metal\n if (!isVMEnvironment()) {\n return url;\n }\n\n // Parse the URL to check the hostname\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid URL — return unchanged\n return url;\n }\n\n // Check if hostname is a localhost variant\n if (!LOCALHOST_HOSTS.has(parsed.hostname)) {\n return url;\n }\n\n // Resolve the gateway host\n const gatewayHost = resolveHostGateway();\n if (!gatewayHost) {\n return url;\n }\n\n // Rewrite the hostname\n parsed.hostname = gatewayHost;\n const rewritten = parsed.toString();\n\n rewriteLogger.debug('Rewrote localhost URL', {\n original: url,\n rewritten,\n gatewayHost,\n });\n\n return rewritten;\n}\n\n/**\n * Reset the cached gateway host (for testing).\n * @internal\n */\nexport function _resetGatewayCache(): void {\n cachedGatewayHost = undefined;\n}\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n"],"mappings":"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","names":[]}
@@ -102,6 +102,24 @@ function resolveHostGateway() {
102
102
  cachedGatewayHost = "host.docker.internal";
103
103
  return cachedGatewayHost;
104
104
  }
105
+ try {
106
+ if (existsSync("/proc/sys/fs/binfmt_misc/WSLInterop")) {
107
+ const resolv = readFileSync("/etc/resolv.conf", "utf-8");
108
+ for (const line of resolv.split("\n")) {
109
+ const trimmed = line.trim();
110
+ if (trimmed.startsWith("#") || trimmed === "") continue;
111
+ const match = trimmed.match(/^nameserver\s+(\S+)/);
112
+ if (match) {
113
+ const ip = match[1];
114
+ rewriteLogger.info("WSL2 host IP from /etc/resolv.conf nameserver", { ip });
115
+ cachedGatewayHost = ip;
116
+ return cachedGatewayHost;
117
+ }
118
+ }
119
+ }
120
+ } catch {
121
+ rewriteLogger.debug("Could not detect WSL2 or read /etc/resolv.conf");
122
+ }
105
123
  try {
106
124
  const routeTable = readFileSync("/proc/net/route", "utf-8");
107
125
  for (const line of routeTable.split("\n").slice(1)) {