@quilltap/plugin-utils 2.5.0 → 2.6.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to @quilltap/plugin-utils will be documented in this file.
4
4
 
5
+ ## [2.6.0] - 2026-08-27
6
+
7
+ ### Removed
8
+
9
+ - **Breaking (behavioral):** `rewriteLocalhostUrl` / `isVMEnvironment` no longer detect Lima or WSL2, and the gateway resolution no longer falls back to `/proc/net/route` or `/etc/hosts`. Quilltap has dropped its managed Lima (macOS) and WSL2 (Windows) VM modes; the supported sandbox is Docker, plus any virtual machine a user builds and manages themselves. Both of the removed fallbacks existed only to serve Lima: the `/proc/net/route` default gateway is the Docker bridge interface, which does *not* reach services on the host's loopback, and `/etc/hosts` can only report a `host.docker.internal` that strategy 2 already assumes.
10
+
11
+ ### Changed
12
+
13
+ - `isVMEnvironment()` is now `isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP`. A hand-rolled VM cannot be auto-detected, so `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and supplies the gateway address. Docker behavior is unchanged: `QUILLTAP_HOST_IP` first, else `host.docker.internal`.
14
+
5
15
  ## [2.5.0] - 2026-08-26
6
16
 
7
17
  ### Added
@@ -1,29 +1,31 @@
1
1
  /**
2
- * Host URL Rewriting for VM/Container Environments
2
+ * Host URL Rewriting for Container Environments
3
3
  *
4
- * When Quilltap runs inside Docker, Lima VMs, or WSL2, `localhost` and
5
- * `127.0.0.1` resolve to the container/VM's own loopback — not the host
6
- * machine where services like Ollama or LM Studio are running.
4
+ * When Quilltap runs inside Docker — or inside a virtual machine the user has
5
+ * built and manages themselves — `localhost` and `127.0.0.1` resolve to the
6
+ * container/VM's own loopback, not the host machine where services like Ollama
7
+ * or LM Studio are running.
7
8
  *
8
9
  * This module provides a single function that transparently rewrites
9
10
  * localhost URLs to point at the host, so users can configure
10
11
  * `http://localhost:11434` and have it Just Work in every environment.
11
12
  *
12
13
  * Gateway resolution order:
13
- * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP
14
- * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`
14
+ * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP.
15
+ * This is also the only supported route for a self-managed VM, where
16
+ * Quilltap has no reliable way to detect the host gateway on its own.
17
+ * 2. In Docker: rewrite `localhost` → `host.docker.internal`
15
18
  * (Docker Desktop DNS or --add-host on Linux handles the forwarding)
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
19
- * NAT networking forwards to host loopback)
20
- * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts
21
- * 5. Give up gracefully — return URL unchanged
19
+ * 3. Give up gracefully — return URL unchanged
22
20
  *
23
21
  * @module @quilltap/plugin-utils/host-rewrite
24
22
  */
25
23
  /**
26
- * Check if running in any VM or container environment that needs URL rewriting.
24
+ * Check if running in an environment that needs URL rewriting.
25
+ *
26
+ * Docker is detected automatically. A hand-rolled VM cannot be, so setting
27
+ * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and
28
+ * supplies the gateway address.
27
29
  */
28
30
  declare function isVMEnvironment(): boolean;
29
31
  /**
@@ -37,7 +39,7 @@ declare function resolveHostGateway(): string | null;
37
39
  * Rewrite a localhost URL to point at the host gateway.
38
40
  *
39
41
  * No-ops when:
40
- * - Not running in a VM/container environment
42
+ * - Not running in a container (and QUILLTAP_HOST_IP is unset)
41
43
  * - The URL doesn't point to localhost or 127.0.0.1
42
44
  * - Gateway resolution fails
43
45
  *
@@ -1,29 +1,31 @@
1
1
  /**
2
- * Host URL Rewriting for VM/Container Environments
2
+ * Host URL Rewriting for Container Environments
3
3
  *
4
- * When Quilltap runs inside Docker, Lima VMs, or WSL2, `localhost` and
5
- * `127.0.0.1` resolve to the container/VM's own loopback — not the host
6
- * machine where services like Ollama or LM Studio are running.
4
+ * When Quilltap runs inside Docker — or inside a virtual machine the user has
5
+ * built and manages themselves — `localhost` and `127.0.0.1` resolve to the
6
+ * container/VM's own loopback, not the host machine where services like Ollama
7
+ * or LM Studio are running.
7
8
  *
8
9
  * This module provides a single function that transparently rewrites
9
10
  * localhost URLs to point at the host, so users can configure
10
11
  * `http://localhost:11434` and have it Just Work in every environment.
11
12
  *
12
13
  * Gateway resolution order:
13
- * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP
14
- * 2. In Docker (not Lima): rewrite `localhost` → `host.docker.internal`
14
+ * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP.
15
+ * This is also the only supported route for a self-managed VM, where
16
+ * Quilltap has no reliable way to detect the host gateway on its own.
17
+ * 2. In Docker: rewrite `localhost` → `host.docker.internal`
15
18
  * (Docker Desktop DNS or --add-host on Linux handles the forwarding)
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
19
- * NAT networking forwards to host loopback)
20
- * 4. Fallback: try DNS lookup of `host.docker.internal` via /etc/hosts
21
- * 5. Give up gracefully — return URL unchanged
19
+ * 3. Give up gracefully — return URL unchanged
22
20
  *
23
21
  * @module @quilltap/plugin-utils/host-rewrite
24
22
  */
25
23
  /**
26
- * Check if running in any VM or container environment that needs URL rewriting.
24
+ * Check if running in an environment that needs URL rewriting.
25
+ *
26
+ * Docker is detected automatically. A hand-rolled VM cannot be, so setting
27
+ * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and
28
+ * supplies the gateway address.
27
29
  */
28
30
  declare function isVMEnvironment(): boolean;
29
31
  /**
@@ -37,7 +39,7 @@ declare function resolveHostGateway(): string | null;
37
39
  * Rewrite a localhost URL to point at the host gateway.
38
40
  *
39
41
  * No-ops when:
40
- * - Not running in a VM/container environment
42
+ * - Not running in a container (and QUILLTAP_HOST_IP is unset)
41
43
  * - The URL doesn't point to localhost or 127.0.0.1
42
44
  * - Gateway resolution fails
43
45
  *
@@ -94,9 +94,6 @@ var LOCALHOST_HOSTS = /* @__PURE__ */ new Set([
94
94
  ]);
95
95
  var cachedGatewayHost;
96
96
  var rewriteLogger = createPluginLogger("host-rewrite");
97
- function isLimaEnvironment() {
98
- return process.env.LIMA_CONTAINER === "true";
99
- }
100
97
  function isDockerEnvironment() {
101
98
  if (process.env.DOCKER_CONTAINER === "true") {
102
99
  return true;
@@ -114,7 +111,7 @@ function isDockerEnvironment() {
114
111
  return false;
115
112
  }
116
113
  function isVMEnvironment() {
117
- return isDockerEnvironment() || isLimaEnvironment();
114
+ return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;
118
115
  }
119
116
  function resolveHostGateway() {
120
117
  if (cachedGatewayHost !== void 0) {
@@ -126,65 +123,11 @@ function resolveHostGateway() {
126
123
  cachedGatewayHost = envIP;
127
124
  return cachedGatewayHost;
128
125
  }
129
- if (isDockerEnvironment() && !isLimaEnvironment()) {
126
+ if (isDockerEnvironment()) {
130
127
  rewriteLogger.info("Docker environment detected \u2014 using host.docker.internal as gateway hostname");
131
128
  cachedGatewayHost = "host.docker.internal";
132
129
  return cachedGatewayHost;
133
130
  }
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
- }
152
- try {
153
- const routeTable = (0, import_node_fs.readFileSync)("/proc/net/route", "utf-8");
154
- for (const line of routeTable.split("\n").slice(1)) {
155
- const fields = line.trim().split(" ");
156
- if (fields.length >= 3 && fields[1] === "00000000") {
157
- const hexGateway = fields[2];
158
- const ip = [
159
- parseInt(hexGateway.substring(6, 8), 16),
160
- parseInt(hexGateway.substring(4, 6), 16),
161
- parseInt(hexGateway.substring(2, 4), 16),
162
- parseInt(hexGateway.substring(0, 2), 16)
163
- ].join(".");
164
- rewriteLogger.info("Host gateway IP from /proc/net/route", { ip });
165
- cachedGatewayHost = ip;
166
- return cachedGatewayHost;
167
- }
168
- }
169
- } catch {
170
- rewriteLogger.debug("Could not read /proc/net/route for default gateway lookup");
171
- }
172
- try {
173
- const hosts = (0, import_node_fs.readFileSync)("/etc/hosts", "utf-8");
174
- for (const line of hosts.split("\n")) {
175
- const trimmed = line.trim();
176
- if (trimmed.startsWith("#") || trimmed === "") continue;
177
- const parts = trimmed.split(/\s+/);
178
- if (parts.length >= 2 && parts.slice(1).includes("host.docker.internal")) {
179
- const ip = parts[0];
180
- rewriteLogger.info("Host gateway IP from /etc/hosts (host.docker.internal)", { ip });
181
- cachedGatewayHost = ip;
182
- return cachedGatewayHost;
183
- }
184
- }
185
- } catch {
186
- rewriteLogger.debug("Could not read /etc/hosts for host.docker.internal lookup");
187
- }
188
131
  rewriteLogger.warn("Could not resolve host gateway \u2014 localhost URLs will not be rewritten");
189
132
  cachedGatewayHost = null;
190
133
  return cachedGatewayHost;
@@ -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 * 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":[]}
1
+ {"version":3,"sources":["../src/host-rewrite.ts","../src/logging/plugin-logger.ts","../src/logging/index.ts"],"sourcesContent":["/**\n * Host URL Rewriting for Container Environments\n *\n * When Quilltap runs inside Docker — or inside a virtual machine the user has\n * built and manages themselves — `localhost` and `127.0.0.1` resolve to the\n * container/VM's own loopback, not the host machine where services like Ollama\n * 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 * This is also the only supported route for a self-managed VM, where\n * Quilltap has no reliable way to detect the host gateway on its own.\n * 2. In Docker: rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 3. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { 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 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 an environment that needs URL rewriting.\n *\n * Docker is detected automatically. A hand-rolled VM cannot be, so setting\n * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and\n * supplies the gateway address.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;\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 // The bridge gateway IP (e.g. 172.17.0.1) is deliberately not used as a\n // fallback: it is only the Docker bridge interface, and services listening\n // on the host's localhost are NOT reachable through it.\n if (isDockerEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\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 container (and QUILLTAP_HOST_IP is unset)\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":[]}
@@ -56,7 +56,7 @@ function createPluginLogger(pluginName, minLevel = "debug") {
56
56
  import { createConsoleLogger, createNoopLogger } from "@quilltap/plugin-types";
57
57
 
58
58
  // src/host-rewrite.ts
59
- import { readFileSync, existsSync, statSync } from "fs";
59
+ import { existsSync, statSync } from "fs";
60
60
  var LOCALHOST_HOSTS = /* @__PURE__ */ new Set([
61
61
  "localhost",
62
62
  "127.0.0.1",
@@ -65,9 +65,6 @@ var LOCALHOST_HOSTS = /* @__PURE__ */ new Set([
65
65
  ]);
66
66
  var cachedGatewayHost;
67
67
  var rewriteLogger = createPluginLogger("host-rewrite");
68
- function isLimaEnvironment() {
69
- return process.env.LIMA_CONTAINER === "true";
70
- }
71
68
  function isDockerEnvironment() {
72
69
  if (process.env.DOCKER_CONTAINER === "true") {
73
70
  return true;
@@ -85,7 +82,7 @@ function isDockerEnvironment() {
85
82
  return false;
86
83
  }
87
84
  function isVMEnvironment() {
88
- return isDockerEnvironment() || isLimaEnvironment();
85
+ return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;
89
86
  }
90
87
  function resolveHostGateway() {
91
88
  if (cachedGatewayHost !== void 0) {
@@ -97,65 +94,11 @@ function resolveHostGateway() {
97
94
  cachedGatewayHost = envIP;
98
95
  return cachedGatewayHost;
99
96
  }
100
- if (isDockerEnvironment() && !isLimaEnvironment()) {
97
+ if (isDockerEnvironment()) {
101
98
  rewriteLogger.info("Docker environment detected \u2014 using host.docker.internal as gateway hostname");
102
99
  cachedGatewayHost = "host.docker.internal";
103
100
  return cachedGatewayHost;
104
101
  }
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
- }
123
- try {
124
- const routeTable = readFileSync("/proc/net/route", "utf-8");
125
- for (const line of routeTable.split("\n").slice(1)) {
126
- const fields = line.trim().split(" ");
127
- if (fields.length >= 3 && fields[1] === "00000000") {
128
- const hexGateway = fields[2];
129
- const ip = [
130
- parseInt(hexGateway.substring(6, 8), 16),
131
- parseInt(hexGateway.substring(4, 6), 16),
132
- parseInt(hexGateway.substring(2, 4), 16),
133
- parseInt(hexGateway.substring(0, 2), 16)
134
- ].join(".");
135
- rewriteLogger.info("Host gateway IP from /proc/net/route", { ip });
136
- cachedGatewayHost = ip;
137
- return cachedGatewayHost;
138
- }
139
- }
140
- } catch {
141
- rewriteLogger.debug("Could not read /proc/net/route for default gateway lookup");
142
- }
143
- try {
144
- const hosts = readFileSync("/etc/hosts", "utf-8");
145
- for (const line of hosts.split("\n")) {
146
- const trimmed = line.trim();
147
- if (trimmed.startsWith("#") || trimmed === "") continue;
148
- const parts = trimmed.split(/\s+/);
149
- if (parts.length >= 2 && parts.slice(1).includes("host.docker.internal")) {
150
- const ip = parts[0];
151
- rewriteLogger.info("Host gateway IP from /etc/hosts (host.docker.internal)", { ip });
152
- cachedGatewayHost = ip;
153
- return cachedGatewayHost;
154
- }
155
- }
156
- } catch {
157
- rewriteLogger.debug("Could not read /etc/hosts for host.docker.internal lookup");
158
- }
159
102
  rewriteLogger.warn("Could not resolve host gateway \u2014 localhost URLs will not be rewritten");
160
103
  cachedGatewayHost = null;
161
104
  return cachedGatewayHost;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logging/plugin-logger.ts","../src/logging/index.ts","../src/host-rewrite.ts"],"sourcesContent":["/**\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","/**\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/logging/plugin-logger.ts","../src/logging/index.ts","../src/host-rewrite.ts"],"sourcesContent":["/**\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","/**\n * Host URL Rewriting for Container Environments\n *\n * When Quilltap runs inside Docker — or inside a virtual machine the user has\n * built and manages themselves — `localhost` and `127.0.0.1` resolve to the\n * container/VM's own loopback, not the host machine where services like Ollama\n * 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 * This is also the only supported route for a self-managed VM, where\n * Quilltap has no reliable way to detect the host gateway on its own.\n * 2. In Docker: rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 3. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { 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 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 an environment that needs URL rewriting.\n *\n * Docker is detected automatically. A hand-rolled VM cannot be, so setting\n * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and\n * supplies the gateway address.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;\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 // The bridge gateway IP (e.g. 172.17.0.1) is deliberately not used as a\n // fallback: it is only the Docker bridge interface, and services listening\n // on the host's localhost are NOT reachable through it.\n if (isDockerEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\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 container (and QUILLTAP_HOST_IP is unset)\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"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -124,6 +124,6 @@ declare function getQuilltapUserAgent(): string;
124
124
  * Version of the plugin-utils package.
125
125
  * Can be used at runtime to check compatibility.
126
126
  */
127
- declare const PLUGIN_UTILS_VERSION = "2.5.0";
127
+ declare const PLUGIN_UTILS_VERSION = "2.6.1";
128
128
 
129
129
  export { BUILTIN_TOOL_NAMES, PLUGIN_UTILS_VERSION, __clearQuilltapVersion, __injectQuilltapVersion, getBuiltinToolNames, getQuilltapUserAgent, getQuilltapVersion };
package/dist/index.d.ts CHANGED
@@ -124,6 +124,6 @@ declare function getQuilltapUserAgent(): string;
124
124
  * Version of the plugin-utils package.
125
125
  * Can be used at runtime to check compatibility.
126
126
  */
127
- declare const PLUGIN_UTILS_VERSION = "2.5.0";
127
+ declare const PLUGIN_UTILS_VERSION = "2.6.1";
128
128
 
129
129
  export { BUILTIN_TOOL_NAMES, PLUGIN_UTILS_VERSION, __clearQuilltapVersion, __injectQuilltapVersion, getBuiltinToolNames, getQuilltapUserAgent, getQuilltapVersion };
package/dist/index.js CHANGED
@@ -998,9 +998,6 @@ var LOCALHOST_HOSTS = /* @__PURE__ */ new Set([
998
998
  ]);
999
999
  var cachedGatewayHost;
1000
1000
  var rewriteLogger = createPluginLogger("host-rewrite");
1001
- function isLimaEnvironment() {
1002
- return process.env.LIMA_CONTAINER === "true";
1003
- }
1004
1001
  function isDockerEnvironment() {
1005
1002
  if (process.env.DOCKER_CONTAINER === "true") {
1006
1003
  return true;
@@ -1018,7 +1015,7 @@ function isDockerEnvironment() {
1018
1015
  return false;
1019
1016
  }
1020
1017
  function isVMEnvironment() {
1021
- return isDockerEnvironment() || isLimaEnvironment();
1018
+ return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;
1022
1019
  }
1023
1020
  function resolveHostGateway() {
1024
1021
  if (cachedGatewayHost !== void 0) {
@@ -1030,65 +1027,11 @@ function resolveHostGateway() {
1030
1027
  cachedGatewayHost = envIP;
1031
1028
  return cachedGatewayHost;
1032
1029
  }
1033
- if (isDockerEnvironment() && !isLimaEnvironment()) {
1030
+ if (isDockerEnvironment()) {
1034
1031
  rewriteLogger.info("Docker environment detected \u2014 using host.docker.internal as gateway hostname");
1035
1032
  cachedGatewayHost = "host.docker.internal";
1036
1033
  return cachedGatewayHost;
1037
1034
  }
1038
- try {
1039
- if ((0, import_node_fs.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop")) {
1040
- const resolv = (0, import_node_fs.readFileSync)("/etc/resolv.conf", "utf-8");
1041
- for (const line of resolv.split("\n")) {
1042
- const trimmed = line.trim();
1043
- if (trimmed.startsWith("#") || trimmed === "") continue;
1044
- const match = trimmed.match(/^nameserver\s+(\S+)/);
1045
- if (match) {
1046
- const ip = match[1];
1047
- rewriteLogger.info("WSL2 host IP from /etc/resolv.conf nameserver", { ip });
1048
- cachedGatewayHost = ip;
1049
- return cachedGatewayHost;
1050
- }
1051
- }
1052
- }
1053
- } catch {
1054
- rewriteLogger.debug("Could not detect WSL2 or read /etc/resolv.conf");
1055
- }
1056
- try {
1057
- const routeTable = (0, import_node_fs.readFileSync)("/proc/net/route", "utf-8");
1058
- for (const line of routeTable.split("\n").slice(1)) {
1059
- const fields = line.trim().split(" ");
1060
- if (fields.length >= 3 && fields[1] === "00000000") {
1061
- const hexGateway = fields[2];
1062
- const ip = [
1063
- parseInt(hexGateway.substring(6, 8), 16),
1064
- parseInt(hexGateway.substring(4, 6), 16),
1065
- parseInt(hexGateway.substring(2, 4), 16),
1066
- parseInt(hexGateway.substring(0, 2), 16)
1067
- ].join(".");
1068
- rewriteLogger.info("Host gateway IP from /proc/net/route", { ip });
1069
- cachedGatewayHost = ip;
1070
- return cachedGatewayHost;
1071
- }
1072
- }
1073
- } catch {
1074
- rewriteLogger.debug("Could not read /proc/net/route for default gateway lookup");
1075
- }
1076
- try {
1077
- const hosts = (0, import_node_fs.readFileSync)("/etc/hosts", "utf-8");
1078
- for (const line of hosts.split("\n")) {
1079
- const trimmed = line.trim();
1080
- if (trimmed.startsWith("#") || trimmed === "") continue;
1081
- const parts = trimmed.split(/\s+/);
1082
- if (parts.length >= 2 && parts.slice(1).includes("host.docker.internal")) {
1083
- const ip = parts[0];
1084
- rewriteLogger.info("Host gateway IP from /etc/hosts (host.docker.internal)", { ip });
1085
- cachedGatewayHost = ip;
1086
- return cachedGatewayHost;
1087
- }
1088
- }
1089
- } catch {
1090
- rewriteLogger.debug("Could not read /etc/hosts for host.docker.internal lookup");
1091
- }
1092
1035
  rewriteLogger.warn("Could not resolve host gateway \u2014 localhost URLs will not be rewritten");
1093
1036
  cachedGatewayHost = null;
1094
1037
  return cachedGatewayHost;
@@ -1121,7 +1064,7 @@ function rewriteLocalhostUrl(url) {
1121
1064
  }
1122
1065
 
1123
1066
  // src/version.generated.ts
1124
- var PLUGIN_UTILS_VERSION = "2.5.0";
1067
+ var PLUGIN_UTILS_VERSION = "2.6.1";
1125
1068
  // Annotate the CommonJS export names for ESM import in node:
1126
1069
  0 && (module.exports = {
1127
1070
  BUILTIN_TOOL_NAMES,