{"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":[]}