{"version":3,"file":"http-serve-9LbcQzVf.mjs","names":["styles.tableContainer","styles.emptyState","styles.table","styles.browserRow","styles.statusActive","styles.timestampCell","styles.actionsCell","styles.killButton","styles.pageRow","styles.urlCell","styles.statsContainer","styles.statCard","styles.container","styles.header","styles.refreshButton","styles.buttonDanger","styles.statCard","styles.tableContainer","styles.emptyState","styles.browserRow","styles.timestampCell","styles.pageRow","styles.urlCell","styles.statusActive","styles.actionsCell","styles.killButton","PLAYWRIGHT_TYPES","BROWSER_IDLE_TIMEOUT_MINUTES_ENV_VAR","ExtensionPageProxy","cleanupOrphanedBrowserTempDirs","createHttpContainer","setWorkspaceRootEnv","buildPlaywrightBaseUrl","getPlaywrightHost","resolveWorkspaceRoot","DEFAULT_MCP_PORT","runWithOwner","assertPageOwnership","OWNER_HEADER","PROFILE_HEADER","getCurrentOwnerId","OUTPUT_DIRECTORY_HEADER","getPackageVersion","resolveSelfProcessIdentity","CLAIM_TOKEN_ENV","telemetryContextFromHeaders","runWithTelemetryContext","TELEMETRY_HEADERS","TelemetryService","PROXY_CONFIG_DIR_ENV_VAR","resolveConfiguredOption","getCommandConfig","createExtensionRoutes","path","REQUIRED_PAGE_ID_FIELD","OPTIONAL_BROWSER_ID_FIELD","ToolCapabilitiesSchema","z","ToolInputSchemaSchema","CustomToolManifestEntrySchema","CustomToolManifestSchema","CUSTOM_TOOL_DEBUG_ENABLED","summarizeValue","value","current","debugCustomTool","details","message","isPlainObject","isCallToolResult","normalizeScalar","rawValue","toAttributes","input","attributes","key","toYamlLines","line","withoutComment","parseYamlValue","lines","startIndex","indent","parseYamlArray","parseYamlObject","result","index","itemText","nextLine","nestedValue","nextIndex","splitYamlKeyValue","objectValue","nestedLine","nestedKey","nestedRawValue","separatorIndex","parseYamlDocument","ensureExecutionTargetSchema","tool","properties","pageIdDefinition","browserIdDefinition","loadCustomToolModule","source","readFile","scriptPath","transform","code","validateCustomToolModule","execute","loadedModule","injectSuggestionActionsIntoObject","injectSuggestionActionsIntoCallToolResult","firstContent","parsed","merged","suggestionActions","normalizeExecutionResult","toolName","normalizedValue","CustomToolService","pageRegistry","extensionTaskQueue","telemetry","browserService","pageEntry","proxy","pageId","entry","currentPageId","browserInstance","browserId","setAsCurrent","options","queued","delegatedText","delegatedPayload","resolvedPageEntry","error","page","startedAt","hasUrl","hasTitle","resolve","requestedPageId","requestedBrowserId","browser","pageHandle","ownerId","ownerPages","baseAttributes","emit","level","directory","candidate","span","SpanStatusCode","logger","rawResult","errorMessage","resolvedDirectory","manifestPath","parsedManifest","manifest","stat","executionHooks","SessionViewScreencast","stream","latest","screenshot","stored","session","payload","at","data","createApiRouter","app","Hono","container","browserInstances","allPages","browsers","browserPages","p","stats","c","formatTimestamp","date","formatDuration","seconds","ms","minutes","hours","formatAge","tableContainer","statCard","browserRow","pageRow","statusActive","urlCell","timestampCell","actionsCell","killButton","emptyState","BrowserTable","Fragment","StatsHeader","Dashboard","raw","Layout","title","createDashboardRouter","REQUEST_CLASS_KEY","LOOPBACK_HOSTS","EXTENSION_ORIGIN_SCHEMES","normalizeHostname","trimmed","end","lastColon","parsePort","hostHeader","afterBracket","port","allowedHostnames","configured","\"BROWSE_TOOL_ALLOWED_HOSTS\"","boundHost","isExtensionOrigin","origin","scheme","isSameOrigin","originPort","boundPort","classifyRequest","headers","hostname","binding","hostPort","secFetchSite","createOriginGuard","config","verdict","next","\"BROWSE_TOOL_DISABLE_ORIGIN_GUARD\"","CALLER_VALIDATION_ERROR_TYPE","SESSION_VIEW_OWNER_PATTERN","SESSION_VIEW_PAGE_NOT_FOUND","SESSION_VIEW_FRAME_CONTENT_TYPES","SESSION_VIEW_FRAME_SEQ_HEADER","readSessionViewOwner","toSessionViewUrl","rawUrl","refreshPageMetadata","timer","cap","getResultError","getStructuredBrowserError","structured","getStructuredToolOutcome","outcome","classifyToolError","isOperationalToolFailure","category","resolveTelemetryService","recordPageActivity","normalizeBuiltInToolName","getStaleToolReplacement","createHttpServer","shuttingDown","customToolService","resolveBinding","cors","name","recordRequest","response","b","requestShutdown","body","tools","normalizedToolName","t","videoDir","replacement","rawArgs","parsedArgs","coerceArgs","validationMessage","formatZodError","validationAttributes","args","requestPageId","structuredError","errorCategory","operationalFailure","errorAttributes","classificationAttributes","ownedPages","instance","view","crash","existing","inFlightFrames","screencast","captureFrame","format","pending","capture","frame","seq","errorName","extensionRouter","apiRouter","dashboardRouter","setupWebSocketRoutes","upgradeWebSocket","connectionId","ws","wsHub","event","conn","isDaemonServing","host","registerSelf","scope","claimToken","confirmed","portRegistry","metadata","requestedPort","existingPort","reserved","DEFAULT_PORT_RANGE","httpServeCommand","Command","BROWSE_TOOL_WORKSPACE_ROOT_ENV","commandDefaults","resolvedOptions","processLease","serviceName","environment","repositoryPath","registryPath","resolveSiblingRegistryPath","\"BROWSE_TOOL_PROXY_CONFIG_DIR\"","processRegistry","browserProcessRegistry","shutdownRequest","idleShutdownRequest","idleCleanupService","nodeWebSocket","createNodeWebSocket","injectWebSocket","taskQueue","sessionRegistry","connection","existingBrowser","existingPages","task","PortRegistryService","finalPort","server","serve","processIdentity","createProcessLease","baseUrl","shutdownPromise","doShutdown","reason","webServerError","processReleaseError","portReleaseError","exitCode","shutdown","cleanupError"],"sources":["../src/services/CustomToolService.ts","../src/services/SessionViewScreencast.ts","../src/server/dashboard/routes/api.ts","../src/server/dashboard/utils/formatters.ts","../src/server/dashboard/components/styles.ts","../src/server/dashboard/components/BrowserTable.tsx","../src/server/dashboard/components/StatsHeader.tsx","../src/server/dashboard/components/Dashboard.tsx","../src/server/dashboard/components/Layout.tsx","../src/server/dashboard/routes/dashboard.tsx","../src/server/middleware/originGuard.ts","../src/server/http.ts","../src/server/websocket-routes.ts","../src/commands/http-serve.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { CallToolResult } from '@modelcontextprotocol/server';\nimport type { Attributes } from '@opentelemetry/api';\nimport { SpanStatusCode } from '@opentelemetry/api';\nimport { transform } from 'esbuild';\nimport { z } from 'zod';\n\nimport { assertPageOwnership, getCurrentOwnerId } from '../utils/ownerContext.js';\nimport type { BrowserMode, IBrowserService } from './BrowserService.js';\nimport { ExtensionPageProxy, type IExtensionPageProxy } from './ExtensionPageProxy.js';\nimport type { ExtensionTaskQueue } from './ExtensionTaskQueue.js';\nimport type { IPageRegistry, PageEntry } from './PageRegistry.js';\nimport { type ITelemetryService, TelemetryService } from './TelemetryService.js';\n\nconst MANIFEST_FILE = 'tools.yaml';\nconst REQUIRED_PAGE_ID_FIELD = 'pageId';\nconst OPTIONAL_BROWSER_ID_FIELD = 'browserId';\n\nconst ToolCapabilitiesSchema = z.record(z.string(), z.unknown());\n\nconst ToolInputSchemaSchema = z\n  .object({\n    type: z.literal('object'),\n    properties: z.record(z.string(), z.unknown()).optional(),\n    required: z.array(z.string()).optional(),\n    additionalProperties: z.boolean().optional(),\n  })\n  .passthrough();\n\nconst CustomToolManifestEntrySchema = z.object({\n  name: z.string().min(1),\n  description: z.string().min(1),\n  script: z.string().min(1),\n  suggestionActions: z.string().min(1).optional(),\n  capabilities: ToolCapabilitiesSchema,\n  inputSchema: ToolInputSchemaSchema,\n});\n\nconst CustomToolManifestSchema = z.object({\n  tools: z.array(CustomToolManifestEntrySchema),\n});\n\ntype CustomToolManifestEntry = z.infer<typeof CustomToolManifestEntrySchema>;\n\nexport interface CustomToolDefinition {\n  name: string;\n  description: string;\n  suggestionActions?: string;\n  inputSchema: z.infer<typeof ToolInputSchemaSchema>;\n  capabilities: z.infer<typeof ToolCapabilitiesSchema>;\n}\n\ninterface LoadedCustomTool extends CustomToolDefinition {\n  scriptPath: string;\n  execute?: CustomToolHandler;\n}\n\ntype CustomToolPage = NonNullable<PageEntry['page']> | IExtensionPageProxy;\n\nexport interface CustomToolBrowserPageSummary {\n  pageId: string;\n  url: string;\n  title: string;\n  active: boolean;\n}\n\nexport interface CustomToolBrowserPageHandle extends CustomToolBrowserPageSummary {\n  page: CustomToolPage;\n}\n\nexport interface CustomToolBrowser {\n  readonly browserId: string;\n  readonly mode: BrowserMode;\n  listPages(): Promise<CustomToolBrowserPageSummary[]>;\n  getPage(pageId: string): Promise<CustomToolBrowserPageHandle>;\n  getCurrentPage(): Promise<CustomToolBrowserPageHandle>;\n  newPage(options?: { url?: string; setAsCurrent?: boolean }): Promise<CustomToolBrowserPageHandle>;\n}\n\nexport interface CustomToolLogOptions {\n  attributes?: Record<string, unknown>;\n  exception?: unknown;\n}\n\nexport interface CustomToolLogger {\n  getTraceContext(): { traceId?: string; spanId?: string };\n  trace(message: string, options?: CustomToolLogOptions): void;\n  debug(message: string, options?: CustomToolLogOptions): void;\n  info(message: string, options?: CustomToolLogOptions): void;\n  warn(message: string, options?: CustomToolLogOptions): void;\n  error(message: string, options?: CustomToolLogOptions): void;\n  fatal(message: string, options?: CustomToolLogOptions): void;\n}\n\ntype CustomToolHandler = (context: {\n  page: CustomToolPage;\n  browser: CustomToolBrowser;\n  input: Record<string, unknown>;\n  logger: CustomToolLogger;\n}) => unknown;\n\ninterface LoadedCustomToolModule {\n  run?: unknown;\n  default?: unknown;\n}\n\ninterface YamlLine {\n  indent: number;\n  text: string;\n}\n\nconst CUSTOM_TOOL_DEBUG_ENABLED = process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS === '1';\nconst CUSTOM_TOOL_PAGE_METADATA_WAIT_TIMEOUT_MS = 1500;\nconst CUSTOM_TOOL_PAGE_METADATA_WAIT_POLL_MS = 50;\n\nfunction summarizeValue(value: unknown): string {\n  return JSON.stringify(value, (_key, current) => {\n    if (typeof current === 'string' && current.length > 240) {\n      return `${current.slice(0, 240)}...<trimmed>`;\n    }\n    return current;\n  });\n}\n\nfunction debugCustomTool(message: string, details?: Record<string, unknown>): void {\n  if (!CUSTOM_TOOL_DEBUG_ENABLED) {\n    return;\n  }\n\n  if (details) {\n    console.error(`[CustomToolService] ${message}`, details);\n    return;\n  }\n\n  console.error(`[CustomToolService] ${message}`);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isCallToolResult(value: unknown): value is CallToolResult {\n  return isPlainObject(value) && Array.isArray(value.content);\n}\n\nfunction normalizeScalar(rawValue: string): unknown {\n  const value = rawValue.trim();\n  if (value === '') {\n    return '';\n  }\n  if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith('[') && value.endsWith(']'))) {\n    return JSON.parse(value);\n  }\n  if (value.startsWith('{') && value.endsWith('}')) {\n    return JSON.parse(value);\n  }\n  if (value.startsWith(\"'\") && value.endsWith(\"'\")) {\n    return value.slice(1, -1);\n  }\n  if (value === 'true') {\n    return true;\n  }\n  if (value === 'false') {\n    return false;\n  }\n  if (value === 'null') {\n    return null;\n  }\n  if (/^-?\\d+(\\.\\d+)?$/.test(value)) {\n    return Number(value);\n  }\n  return value;\n}\n\nfunction toAttributes(input: Record<string, unknown> | undefined): Attributes | undefined {\n  if (!input || Object.keys(input).length === 0) {\n    return undefined;\n  }\n\n  const attributes: Attributes = {};\n  for (const [key, value] of Object.entries(input)) {\n    if (value === undefined || value === null) {\n      continue;\n    }\n\n    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n      attributes[key] = value;\n      continue;\n    }\n\n    attributes[key] = JSON.stringify(value);\n  }\n\n  return Object.keys(attributes).length > 0 ? attributes : undefined;\n}\n\nfunction toYamlLines(input: string): YamlLine[] {\n  return input\n    .replace(/^\\uFEFF/, '')\n    .split(/\\r?\\n/)\n    .map((line) => {\n      if (line.includes('\\t')) {\n        throw new Error('Tab indentation is not supported in tools.yaml');\n      }\n\n      const withoutComment = line.replace(/\\s+#.*$/, '');\n      if (withoutComment.trim().length === 0) {\n        return null;\n      }\n\n      const indent = withoutComment.match(/^ */)?.[0].length ?? 0;\n      return {\n        indent,\n        text: withoutComment.trim(),\n      };\n    })\n    .filter((line): line is YamlLine => line !== null);\n}\n\nfunction parseYamlValue(lines: YamlLine[], startIndex: number, indent: number): [unknown, number] {\n  const line = lines[startIndex];\n  if (!line || line.indent !== indent) {\n    throw new Error(`Invalid indentation in tools.yaml at line ${startIndex + 1}`);\n  }\n\n  if (line.text.startsWith('-')) {\n    return parseYamlArray(lines, startIndex, indent);\n  }\n\n  return parseYamlObject(lines, startIndex, indent);\n}\n\nfunction parseYamlArray(lines: YamlLine[], startIndex: number, indent: number): [unknown[], number] {\n  const result: unknown[] = [];\n  let index = startIndex;\n\n  while (index < lines.length) {\n    const line = lines[index];\n    if (line.indent < indent) {\n      break;\n    }\n    if (line.indent !== indent || !line.text.startsWith('-')) {\n      break;\n    }\n\n    const itemText = line.text.slice(1).trim();\n    if (itemText === '') {\n      const nextLine = lines[index + 1];\n      if (!nextLine || nextLine.indent <= indent) {\n        result.push(null);\n        index += 1;\n        continue;\n      }\n      const [nestedValue, nextIndex] = parseYamlValue(lines, index + 1, nextLine.indent);\n      result.push(nestedValue);\n      index = nextIndex;\n      continue;\n    }\n\n    if (itemText.includes(':')) {\n      const [key, rawValue] = splitYamlKeyValue(itemText);\n      const objectValue: Record<string, unknown> = {};\n\n      if (rawValue === undefined) {\n        const nextLine = lines[index + 1];\n        if (!nextLine || nextLine.indent <= indent) {\n          throw new Error(`Expected nested value for \"${key}\" in tools.yaml`);\n        }\n        const [nestedValue, nextIndex] = parseYamlValue(lines, index + 1, nextLine.indent);\n        objectValue[key] = nestedValue;\n        index = nextIndex;\n      } else {\n        objectValue[key] = normalizeScalar(rawValue);\n        index += 1;\n      }\n\n      while (index < lines.length && lines[index].indent > indent) {\n        const nestedLine = lines[index];\n        if (nestedLine.indent !== indent + 2 || nestedLine.text.startsWith('-')) {\n          const [nestedValue, nextIndex] = parseYamlValue(lines, index, nestedLine.indent);\n          if (!isPlainObject(nestedValue)) {\n            throw new Error(`Expected object entry in tools.yaml at line ${index + 1}`);\n          }\n          Object.assign(objectValue, nestedValue);\n          index = nextIndex;\n          continue;\n        }\n\n        const [nestedKey, nestedRawValue] = splitYamlKeyValue(nestedLine.text);\n        if (nestedRawValue === undefined) {\n          const nextLine = lines[index + 1];\n          if (!nextLine || nextLine.indent <= nestedLine.indent) {\n            throw new Error(`Expected nested value for \"${nestedKey}\" in tools.yaml`);\n          }\n          const [nestedValue, nextIndex] = parseYamlValue(lines, index + 1, nextLine.indent);\n          objectValue[nestedKey] = nestedValue;\n          index = nextIndex;\n          continue;\n        }\n\n        objectValue[nestedKey] = normalizeScalar(nestedRawValue);\n        index += 1;\n      }\n\n      result.push(objectValue);\n      continue;\n    }\n\n    result.push(normalizeScalar(itemText));\n    index += 1;\n  }\n\n  return [result, index];\n}\n\nfunction parseYamlObject(lines: YamlLine[], startIndex: number, indent: number): [Record<string, unknown>, number] {\n  const result: Record<string, unknown> = {};\n  let index = startIndex;\n\n  while (index < lines.length) {\n    const line = lines[index];\n    if (line.indent < indent) {\n      break;\n    }\n    if (line.indent !== indent || line.text.startsWith('-')) {\n      break;\n    }\n\n    const [key, rawValue] = splitYamlKeyValue(line.text);\n\n    if (rawValue === undefined) {\n      const nextLine = lines[index + 1];\n      if (!nextLine || nextLine.indent <= indent) {\n        result[key] = null;\n        index += 1;\n        continue;\n      }\n\n      const [nestedValue, nextIndex] = parseYamlValue(lines, index + 1, nextLine.indent);\n      result[key] = nestedValue;\n      index = nextIndex;\n      continue;\n    }\n\n    result[key] = normalizeScalar(rawValue);\n    index += 1;\n  }\n\n  return [result, index];\n}\n\nfunction splitYamlKeyValue(input: string): [string, string | undefined] {\n  const separatorIndex = input.indexOf(':');\n  if (separatorIndex === -1) {\n    throw new Error(`Invalid tools.yaml entry: \"${input}\"`);\n  }\n\n  const key = input.slice(0, separatorIndex).trim();\n  const rawValue = input.slice(separatorIndex + 1).trim();\n  return [key, rawValue === '' ? undefined : rawValue];\n}\n\nfunction parseYamlDocument(input: string): unknown {\n  const lines = toYamlLines(input);\n  if (lines.length === 0) {\n    return {};\n  }\n  const [value] = parseYamlValue(lines, 0, lines[0].indent);\n  return value;\n}\n\nfunction ensureExecutionTargetSchema(tool: CustomToolManifestEntry): void {\n  if (tool.inputSchema.properties === undefined) {\n    tool.inputSchema.properties = {};\n  }\n  const properties = tool.inputSchema.properties;\n\n  if (!isPlainObject(properties)) {\n    throw new Error(`Custom tool \"${tool.name}\" inputSchema.properties must be an object`);\n  }\n\n  const pageIdDefinition = properties[REQUIRED_PAGE_ID_FIELD];\n  const browserIdDefinition = properties[OPTIONAL_BROWSER_ID_FIELD];\n\n  if (pageIdDefinition !== undefined && !(isPlainObject(pageIdDefinition) && pageIdDefinition.type === 'string')) {\n    throw new Error(`Custom tool \"${tool.name}\" must define inputSchema.properties.pageId as type \"string\"`);\n  }\n\n  if (\n    browserIdDefinition !== undefined &&\n    !(isPlainObject(browserIdDefinition) && browserIdDefinition.type === 'string')\n  ) {\n    throw new Error(`Custom tool \"${tool.name}\" must define inputSchema.properties.browserId as type \"string\"`);\n  }\n\n  if (pageIdDefinition === undefined) {\n    properties[REQUIRED_PAGE_ID_FIELD] = {\n      type: 'string',\n      description:\n        'Browse-tool page ID to run this custom tool against. Either pageId or browserId is required at call time.',\n    };\n  }\n\n  if (browserIdDefinition === undefined) {\n    properties[OPTIONAL_BROWSER_ID_FIELD] = {\n      type: 'string',\n      description:\n        \"Browse-tool browser ID to run this custom tool against when no pageId is supplied. The tool will use the browser's current page or open a new one.\",\n    };\n  }\n}\n\nasync function loadCustomToolModule(scriptPath: string): Promise<LoadedCustomToolModule> {\n  const source = await readFile(scriptPath, 'utf8');\n  const { code } = await transform(source, { loader: 'ts', format: 'esm', target: 'node20' });\n  const moduleUrl = `data:text/javascript;base64,${Buffer.from(code, 'utf8').toString('base64')}`;\n  return (await import(moduleUrl)) as LoadedCustomToolModule;\n}\n\nfunction validateCustomToolModule(\n  scriptPath: string,\n  loadedModule: LoadedCustomToolModule,\n): {\n  execute?: CustomToolHandler;\n} {\n  const execute = typeof loadedModule.run === 'function' ? (loadedModule.run as CustomToolHandler) : undefined;\n\n  if (!execute) {\n    throw new Error(`Custom tool script \"${scriptPath}\" must export a \"run\" function`);\n  }\n\n  return { execute };\n}\n\nfunction injectSuggestionActionsIntoObject(\n  value: Record<string, unknown>,\n  suggestionActions: string,\n): Record<string, unknown> {\n  if (typeof value.suggestionActions === 'string' && value.suggestionActions.trim().length > 0) {\n    return value;\n  }\n\n  return {\n    ...value,\n    suggestionActions,\n  };\n}\n\nfunction injectSuggestionActionsIntoCallToolResult(result: CallToolResult, suggestionActions: string): CallToolResult {\n  const firstContent = result.content[0];\n  if (firstContent?.type === 'text' && typeof firstContent.text === 'string') {\n    try {\n      const parsed = JSON.parse(firstContent.text);\n      if (isPlainObject(parsed)) {\n        const merged = injectSuggestionActionsIntoObject(parsed, suggestionActions);\n        return {\n          ...result,\n          content: [{ ...firstContent, text: JSON.stringify(merged, null, 2) }, ...result.content.slice(1)],\n        };\n      }\n    } catch {\n      // Keep the original text payload when it is not valid JSON.\n    }\n  }\n\n  return {\n    ...result,\n    content: [\n      ...result.content,\n      {\n        type: 'text',\n        text: `suggestionActions: ${suggestionActions}`,\n      },\n    ],\n  };\n}\n\nfunction normalizeExecutionResult(toolName: string, value: unknown, suggestionActions?: string): CallToolResult {\n  if (isCallToolResult(value)) {\n    return suggestionActions ? injectSuggestionActionsIntoCallToolResult(value, suggestionActions) : value;\n  }\n\n  if (typeof value === 'string') {\n    if (!suggestionActions) {\n      return { content: [{ type: 'text', text: value }] };\n    }\n\n    return {\n      content: [\n        {\n          type: 'text',\n          text: JSON.stringify({ result: value, suggestionActions }, null, 2),\n        },\n      ],\n    };\n  }\n\n  if (value === undefined) {\n    if (!suggestionActions) {\n      return { content: [{ type: 'text', text: `Custom tool \"${toolName}\" completed successfully` }] };\n    }\n\n    return {\n      content: [\n        {\n          type: 'text',\n          text: JSON.stringify(\n            {\n              result: `Custom tool \"${toolName}\" completed successfully`,\n              suggestionActions,\n            },\n            null,\n            2,\n          ),\n        },\n      ],\n    };\n  }\n\n  const normalizedValue =\n    isPlainObject(value) && suggestionActions ? injectSuggestionActionsIntoObject(value, suggestionActions) : value;\n  return {\n    content: [{ type: 'text', text: JSON.stringify(normalizedValue, null, 2) }],\n  };\n}\n\nexport class CustomToolService {\n  constructor(\n    private readonly pageRegistry: IPageRegistry,\n    private readonly extensionTaskQueue?: ExtensionTaskQueue,\n    private readonly telemetry: ITelemetryService = new TelemetryService(),\n    private readonly browserService?: IBrowserService,\n  ) {}\n\n  private resolveToolPage(toolName: string, pageId: string, pageEntry: PageEntry): CustomToolPage {\n    if (pageEntry.page) {\n      return pageEntry.page;\n    }\n\n    if (pageEntry.mode === 'extension' && this.extensionTaskQueue) {\n      const proxy = new ExtensionPageProxy(this.extensionTaskQueue);\n      proxy.setTarget(pageId, pageEntry.browserId);\n      return proxy;\n    }\n\n    throw new Error(`Custom tool \"${toolName}\" requires a supported page context`);\n  }\n\n  private toPageSummary(currentPageId: string | null, entry: PageEntry): CustomToolBrowserPageSummary {\n    return {\n      pageId: entry.id,\n      url: entry.url,\n      title: entry.title,\n      active: currentPageId === entry.id,\n    };\n  }\n\n  private async createPageForBrowser(\n    toolName: string,\n    browserId: string,\n    options?: { url?: string; setAsCurrent?: boolean },\n  ): Promise<CustomToolBrowserPageHandle> {\n    if (!this.browserService) {\n      throw new Error(`Custom tool \"${toolName}\" requires browser service support to create a page`);\n    }\n\n    const browserInstance = this.browserService.getBrowser(browserId);\n    if (!browserInstance) {\n      throw new Error(`Browser \"${browserId}\" not found`);\n    }\n\n    const setAsCurrent = options?.setAsCurrent !== false;\n\n    if (browserInstance.mode === 'extension' || browserInstance.mode === 'vm') {\n      if (!this.extensionTaskQueue) {\n        throw new Error(`Custom tool \"${toolName}\" requires extension task support to create a page`);\n      }\n\n      const pageId = this.pageRegistry.registerExtensionPage(browserId, undefined, options?.url, false);\n\n      try {\n        const queued = await this.extensionTaskQueue.queueTask(\n          'browser_new_page',\n          {\n            browserId,\n            pageId,\n            url: options?.url,\n            setAsCurrent,\n          },\n          10_000,\n          browserId,\n        );\n\n        // A delivered task can still report failure (e.g. the tab could not be\n        // opened), so an accepted-but-errored result must not register a page.\n        if (!queued.success || queued.result?.isError === true) {\n          throw new Error(queued.error ?? `Custom tool \"${toolName}\" failed to create a page`);\n        }\n\n        const delegatedText = queued.result?.content[0]?.type === 'text' ? queued.result.content[0].text : undefined;\n        let delegatedPayload: { url?: string; title?: string; tabId?: number } = {};\n        if (typeof delegatedText === 'string' && delegatedText.length > 0) {\n          try {\n            delegatedPayload = JSON.parse(delegatedText) as { url?: string; title?: string; tabId?: number };\n          } catch {\n            delegatedPayload = {};\n          }\n        }\n\n        const pageEntry = this.pageRegistry.get(pageId);\n        if (!pageEntry) {\n          throw new Error(`Page \"${pageId}\" was not registered`);\n        }\n\n        pageEntry.url = delegatedPayload.url ?? pageEntry.url;\n        pageEntry.title = delegatedPayload.title ?? pageEntry.title;\n        pageEntry.extensionTabId = delegatedPayload.tabId ?? pageEntry.extensionTabId;\n        const resolvedPageEntry = await this.waitForResolvedPageMetadata(pageId);\n        browserInstance.pageIds.add(pageId);\n        if (setAsCurrent || !browserInstance.currentPageId) {\n          this.browserService.setCurrentPage(browserId, pageId);\n        }\n        this.browserService.recordBrowserActivity(browserId, pageId);\n\n        return {\n          ...this.toPageSummary(browserInstance.currentPageId, resolvedPageEntry),\n          page: this.resolveToolPage(toolName, pageId, resolvedPageEntry),\n        };\n      } catch (error) {\n        this.pageRegistry.remove(pageId);\n        throw error;\n      }\n    }\n\n    const { pageId, page } = await this.browserService.newPage(browserId);\n    if (options?.url) {\n      await page.goto(options.url);\n      await this.pageRegistry.updateMetadata(pageId);\n    }\n\n    if (setAsCurrent) {\n      this.browserService.setCurrentPage(browserId, pageId);\n    }\n    this.browserService.recordBrowserActivity(browserId, pageId);\n\n    const pageEntry = this.pageRegistry.get(pageId);\n    if (!pageEntry) {\n      throw new Error(`Page \"${pageId}\" was not registered`);\n    }\n\n    return {\n      ...this.toPageSummary(browserInstance.currentPageId, pageEntry),\n      page: this.resolveToolPage(toolName, pageId, pageEntry),\n    };\n  }\n\n  private async waitForResolvedPageMetadata(pageId: string): Promise<PageEntry> {\n    const startedAt = Date.now();\n    let entry = this.pageRegistry.get(pageId);\n\n    while (entry && Date.now() - startedAt < CUSTOM_TOOL_PAGE_METADATA_WAIT_TIMEOUT_MS) {\n      const hasUrl = typeof entry.url === 'string' && entry.url.length > 0;\n      const hasTitle = typeof entry.title === 'string' && entry.title.length > 0 && entry.title !== 'Extension Tab';\n      if (hasUrl && hasTitle) {\n        return entry;\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, CUSTOM_TOOL_PAGE_METADATA_WAIT_POLL_MS));\n      entry = this.pageRegistry.get(pageId);\n    }\n\n    if (!entry) {\n      throw new Error(`Page \"${pageId}\" was not registered`);\n    }\n\n    return entry;\n  }\n\n  private async resolveExecutionContext(\n    toolName: string,\n    input: Record<string, unknown>,\n  ): Promise<{ pageId: string; pageEntry: PageEntry; page: CustomToolPage; browser: CustomToolBrowser }> {\n    const requestedPageId =\n      typeof input[REQUIRED_PAGE_ID_FIELD] === 'string' && input[REQUIRED_PAGE_ID_FIELD].length > 0\n        ? input[REQUIRED_PAGE_ID_FIELD]\n        : undefined;\n    const requestedBrowserId =\n      typeof input[OPTIONAL_BROWSER_ID_FIELD] === 'string' && input[OPTIONAL_BROWSER_ID_FIELD].length > 0\n        ? input[OPTIONAL_BROWSER_ID_FIELD]\n        : undefined;\n\n    if (!requestedPageId && !requestedBrowserId) {\n      throw new Error(`Custom tool \"${toolName}\" requires a string pageId or browserId`);\n    }\n\n    if (requestedPageId) {\n      const pageEntry = this.pageRegistry.get(requestedPageId);\n      if (!pageEntry) {\n        throw new Error(`Page \"${requestedPageId}\" not found`);\n      }\n      assertPageOwnership(pageEntry, `Custom tool \"${toolName}\"`);\n      if (requestedBrowserId && pageEntry.browserId !== requestedBrowserId) {\n        throw new Error(\n          `Custom tool \"${toolName}\" received pageId \"${requestedPageId}\" for browser \"${pageEntry.browserId}\", not \"${requestedBrowserId}\"`,\n        );\n      }\n\n      const browser = this.createBrowserHelper(toolName, pageEntry.browserId);\n      return {\n        pageId: requestedPageId,\n        pageEntry,\n        page: this.resolveToolPage(toolName, requestedPageId, pageEntry),\n        browser,\n      };\n    }\n\n    const browser = this.createBrowserHelper(toolName, requestedBrowserId as string);\n    try {\n      const pageHandle = await browser.getCurrentPage();\n      const pageEntry = this.pageRegistry.get(pageHandle.pageId);\n      if (!pageEntry) {\n        throw new Error(`Page \"${pageHandle.pageId}\" not found`);\n      }\n\n      return {\n        pageId: pageHandle.pageId,\n        pageEntry,\n        page: pageHandle.page,\n        browser,\n      };\n    } catch (error) {\n      const message = error instanceof Error ? error.message : String(error);\n      if (!message.includes('has no pages')) {\n        throw error;\n      }\n\n      const pageHandle = await browser.newPage();\n      const pageEntry = this.pageRegistry.get(pageHandle.pageId);\n      if (!pageEntry) {\n        throw new Error(`Page \"${pageHandle.pageId}\" not found`, {\n          cause: error,\n        });\n      }\n\n      return {\n        pageId: pageHandle.pageId,\n        pageEntry,\n        page: pageHandle.page,\n        browser,\n      };\n    }\n  }\n\n  private createBrowserHelper(toolName: string, browserId: string): CustomToolBrowser {\n    const listPages = async (): Promise<CustomToolBrowserPageSummary[]> => {\n      const browserInstance = this.browserService?.getBrowser(browserId);\n      if (!browserInstance) {\n        throw new Error(`Browser \"${browserId}\" not found`);\n      }\n\n      return this.pageRegistry\n        .findByBrowser(browserId)\n        .map((entry) => this.toPageSummary(browserInstance.currentPageId, entry));\n    };\n\n    return {\n      browserId,\n      mode: this.browserService?.getBrowser(browserId)?.mode ?? 'extension',\n      listPages,\n      getPage: async (pageId: string): Promise<CustomToolBrowserPageHandle> => {\n        const pageEntry = this.pageRegistry.get(pageId);\n        if (!pageEntry || pageEntry.browserId !== browserId) {\n          throw new Error(`Page \"${pageId}\" not found in browser \"${browserId}\"`);\n        }\n\n        const browserInstance = this.browserService?.getBrowser(browserId);\n        return {\n          ...this.toPageSummary(browserInstance?.currentPageId ?? null, pageEntry),\n          page: this.resolveToolPage(toolName, pageId, pageEntry),\n        };\n      },\n      getCurrentPage: async (): Promise<CustomToolBrowserPageHandle> => {\n        const browserInstance = this.browserService?.getBrowser(browserId);\n        if (!browserInstance) {\n          throw new Error(`Browser \"${browserId}\" not found`);\n        }\n\n        const ownerId = getCurrentOwnerId();\n        let currentPageId: string | undefined;\n        if (ownerId) {\n          // Scope \"current page\" to the calling session so concurrent agents sharing\n          // this profile browser never resolve each other's tab.\n          const ownerPages = this.pageRegistry.findByOwner(ownerId).filter((entry) => entry.browserId === browserId);\n          currentPageId = ownerPages[ownerPages.length - 1]?.id;\n          if (!currentPageId) {\n            throw new Error(`Browser \"${browserId}\" has no pages owned by this session`);\n          }\n        } else {\n          currentPageId =\n            browserInstance.currentPageId ?? this.pageRegistry.findByBrowser(browserId)[0]?.id ?? undefined;\n        }\n        if (!currentPageId) {\n          throw new Error(`Browser \"${browserId}\" has no pages`);\n        }\n\n        const pageEntry = this.pageRegistry.get(currentPageId);\n        if (!pageEntry) {\n          throw new Error(`Page \"${currentPageId}\" not found`);\n        }\n\n        return {\n          ...this.toPageSummary(browserInstance.currentPageId, pageEntry),\n          page: this.resolveToolPage(toolName, currentPageId, pageEntry),\n        };\n      },\n      newPage: async (options?: { url?: string; setAsCurrent?: boolean }): Promise<CustomToolBrowserPageHandle> =>\n        this.createPageForBrowser(toolName, browserId, options),\n    };\n  }\n\n  private createToolLogger(toolName: string, pageId: string, pageEntry: PageEntry): CustomToolLogger {\n    const baseAttributes: Record<string, unknown> = {\n      'browse_tool.tool.name': toolName,\n      'browse_tool.page.id': pageId,\n      'browse_tool.browser.id': pageEntry.browserId,\n      'browse_tool.execution.mode': pageEntry.mode,\n    };\n\n    const emit = (\n      level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal',\n      message: string,\n      options?: CustomToolLogOptions,\n    ): void => {\n      this.telemetry.log(level, message, {\n        attributes: toAttributes({\n          ...baseAttributes,\n          ...options?.attributes,\n        }),\n        exception: options?.exception,\n      });\n    };\n\n    return {\n      getTraceContext: () => this.telemetry.getActiveTraceContext(),\n      trace: (message, options) => emit('trace', message, options),\n      debug: (message, options) => emit('debug', message, options),\n      info: (message, options) => emit('info', message, options),\n      warn: (message, options) => emit('warn', message, options),\n      error: (message, options) => emit('error', message, options),\n      fatal: (message, options) => emit('fatal', message, options),\n    };\n  }\n\n  async listTools(directory: string): Promise<CustomToolDefinition[]> {\n    const tools = await this.loadTools(directory);\n    return tools.map(({ name, description, suggestionActions, inputSchema, capabilities }) => ({\n      name,\n      description,\n      suggestionActions,\n      inputSchema,\n      capabilities,\n    }));\n  }\n\n  async executeTool(directory: string, toolName: string, input: Record<string, unknown>): Promise<CallToolResult> {\n    const requestedPageId =\n      typeof input[REQUIRED_PAGE_ID_FIELD] === 'string' ? input[REQUIRED_PAGE_ID_FIELD] : undefined;\n    const requestedBrowserId =\n      typeof input[OPTIONAL_BROWSER_ID_FIELD] === 'string' ? input[OPTIONAL_BROWSER_ID_FIELD] : undefined;\n\n    return this.telemetry.runInSpan(\n      'browse_tool.custom_tool.execute',\n      {\n        attributes: {\n          'browse_tool.tool.name': toolName,\n          'browse_tool.custom_tools.directory': path.resolve(directory),\n          'browse_tool.page.id': requestedPageId,\n          'browse_tool.browser.id': requestedBrowserId,\n        },\n      },\n      async (span) => {\n        const tools = await this.loadTools(directory);\n        const tool = tools.find((candidate) => candidate.name === toolName);\n\n        if (!tool) {\n          span?.setStatus({ code: SpanStatusCode.ERROR, message: `Custom tool \"${toolName}\" not found` });\n          throw new Error(`Custom tool \"${toolName}\" not found`);\n        }\n\n        const { pageId, pageEntry, page, browser } = await this.resolveExecutionContext(toolName, input);\n        const logger = this.createToolLogger(toolName, pageId, pageEntry);\n        span?.setAttributes({\n          'browse_tool.browser.id': pageEntry.browserId,\n          'browse_tool.execution.mode': pageEntry.mode,\n          'browse_tool.page.id': pageId,\n        });\n        debugCustomTool('Executing custom tool', {\n          toolName,\n          directory: path.resolve(directory),\n          pageId,\n          browserId: pageEntry.browserId,\n          mode: pageEntry.mode,\n          input: summarizeValue(input),\n          scriptPath: tool.scriptPath,\n        });\n\n        let rawResult;\n        try {\n          rawResult = await tool.execute?.({ page, browser, input, logger });\n        } catch (error) {\n          const message = error instanceof Error ? error.message : String(error);\n          debugCustomTool('Custom tool execution failed', {\n            toolName,\n            pageId,\n            browserId: pageEntry.browserId,\n            mode: pageEntry.mode,\n            error: message,\n            stack: error instanceof Error ? error.stack : undefined,\n          });\n          throw new Error(`Custom tool \"${toolName}\" failed on page \"${pageId}\": ${message}`, {\n            cause: error,\n          });\n        }\n\n        debugCustomTool('Custom tool execution completed', {\n          toolName,\n          pageId,\n          browserId: pageEntry.browserId,\n          mode: pageEntry.mode,\n          result: summarizeValue(rawResult),\n        });\n\n        const result = normalizeExecutionResult(toolName, rawResult, tool.suggestionActions);\n        const errorMessage = result.isError ? (result.content[0] as { text?: string })?.text : undefined;\n        if (errorMessage) {\n          span?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });\n        }\n\n        return result;\n      },\n    );\n  }\n\n  private async loadTools(directory: string): Promise<LoadedCustomTool[]> {\n    const resolvedDirectory = path.resolve(directory);\n    const manifestPath = path.join(resolvedDirectory, MANIFEST_FILE);\n    const manifestSource = await readFile(manifestPath, 'utf8').catch((error) => {\n      throw new Error(\n        `Failed to read custom tool manifest at \"${manifestPath}\": ${error instanceof Error ? error.message : String(error)}`,\n      );\n    });\n\n    const parsedManifest = parseYamlDocument(manifestSource);\n    const manifest = CustomToolManifestSchema.parse(parsedManifest);\n\n    return Promise.all(\n      manifest.tools.map(async (tool) => {\n        ensureExecutionTargetSchema(tool);\n\n        const scriptPath = path.resolve(resolvedDirectory, tool.script);\n        await stat(scriptPath).catch((error) => {\n          throw new Error(\n            `Custom tool \"${tool.name}\" script not found at \"${scriptPath}\": ${error instanceof Error ? error.message : String(error)}`,\n          );\n        });\n\n        const loadedModule = await loadCustomToolModule(scriptPath);\n        const executionHooks = validateCustomToolModule(scriptPath, loadedModule);\n\n        return {\n          name: tool.name,\n          description: tool.description,\n          suggestionActions: tool.suggestionActions,\n          inputSchema: tool.inputSchema,\n          capabilities: tool.capabilities,\n          scriptPath,\n          ...executionHooks,\n        };\n      }),\n    );\n  }\n}\n","import type { CDPSession, Page } from 'playwright';\n\n/** One cached JPEG and its number. The number changes only when the bytes do, and is never reused. */\nexport interface SessionViewFrame {\n  seq: number;\n  data: Buffer;\n}\n\nexport interface SessionViewScreencastOptions {\n  /** JPEG quality for screencast frames, matching the screenshot path. */\n  quality: number;\n  /** Stop a page's screencast after this long without a fetch. */\n  idleMs?: number;\n  /** Past this age the cached frame is replaced by a fresh screenshot. */\n  staleMs?: number;\n  now?: () => number;\n}\n\ninterface ScreencastFramePayload {\n  data: string;\n  sessionId: number;\n}\n\ninterface PageStream {\n  session?: CDPSession;\n  starting?: Promise<void>;\n  /** Not Chromium, or the CDP session refused to start: screenshots only. */\n  unsupported: boolean;\n  latest?: SessionViewFrame & { at: number };\n  idleTimer?: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_IDLE_MS = 2000;\nconst DEFAULT_STALE_MS = 1000;\n\n/**\n * Live frames for the session view. A page's CDP screencast starts on the\n * first fetch, every frame is acknowledged at once so Chrome keeps sending,\n * and only the latest is kept. Viewers pull at their own pace, so a slow\n * viewer drops frames instead of queueing them anywhere.\n *\n * Chrome sends nothing while nothing repaints, and may not repaint a hidden\n * or covered window at all, so a frame older than `staleMs` is replaced by a\n * screenshot. Identical bytes keep the same number.\n */\nexport class SessionViewScreencast {\n  private readonly streams = new Map<string, PageStream>();\n  /** Shared across pages and restarts, so a number a viewer holds is never reused. */\n  private seq = 0;\n  private readonly idleMs: number;\n  private readonly staleMs: number;\n  private readonly now: () => number;\n\n  constructor(private readonly options: SessionViewScreencastOptions) {\n    this.idleMs = options.idleMs ?? DEFAULT_IDLE_MS;\n    this.staleMs = options.staleMs ?? DEFAULT_STALE_MS;\n    this.now = options.now ?? Date.now;\n  }\n\n  /** The latest frame of a page; `screenshot` runs when no fresh screencast frame exists. */\n  async frame(pageId: string, page: Page, screenshot: () => Promise<Buffer>): Promise<SessionViewFrame> {\n    let stream = this.streams.get(pageId);\n    if (!stream) {\n      stream = { unsupported: false };\n      this.streams.set(pageId, stream);\n    }\n    this.keepAlive(pageId, stream);\n    if (!stream.unsupported && !stream.session) {\n      stream.starting ??= this.start(pageId, page, stream).finally(() => {\n        stream.starting = undefined;\n      });\n      await stream.starting;\n    }\n    const latest = stream.latest;\n    if (latest && this.now() - latest.at < this.staleMs) return { seq: latest.seq, data: latest.data };\n    this.store(stream, await screenshot());\n    const stored = stream.latest!;\n    return { seq: stored.seq, data: stored.data };\n  }\n\n  private keepAlive(pageId: string, stream: PageStream): void {\n    clearTimeout(stream.idleTimer);\n    stream.idleTimer = setTimeout(() => void this.stop(pageId), this.idleMs);\n    stream.idleTimer.unref?.();\n  }\n\n  private async start(pageId: string, page: Page, stream: PageStream): Promise<void> {\n    let session: CDPSession;\n    try {\n      session = await page.context().newCDPSession(page);\n    } catch {\n      // Firefox and WebKit have no CDP session; screenshots still work there.\n      stream.unsupported = true;\n      return;\n    }\n    session.on('Page.screencastFrame', (payload: ScreencastFramePayload) => {\n      if (this.streams.get(pageId) !== stream) return;\n      this.store(stream, Buffer.from(payload.data, 'base64'));\n      session.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {\n        // The page or session went away mid-frame; the next fetch screenshots or restarts.\n        void this.stop(pageId);\n      });\n    });\n    try {\n      await session.send('Page.startScreencast', { format: 'jpeg', quality: this.options.quality });\n    } catch {\n      // A Chromium build that refuses screencasts falls back to screenshots for this page.\n      stream.unsupported = true;\n      await session.detach().catch(() => undefined);\n      return;\n    }\n    if (this.streams.get(pageId) !== stream) {\n      // Stopped while starting: nobody owns this session, so end it here.\n      await session.send('Page.stopScreencast').catch(() => undefined);\n      await session.detach().catch(() => undefined);\n      return;\n    }\n    stream.session = session;\n    page.once('close', () => void this.stop(pageId));\n  }\n\n  private store(stream: PageStream, data: Buffer): void {\n    const at = this.now();\n    if (stream.latest?.data.equals(data)) {\n      stream.latest.at = at;\n      return;\n    }\n    stream.latest = { seq: ++this.seq, data, at };\n  }\n\n  private async stop(pageId: string): Promise<void> {\n    const stream = this.streams.get(pageId);\n    if (!stream) return;\n    this.streams.delete(pageId);\n    clearTimeout(stream.idleTimer);\n    const session = stream.session;\n    if (!session) return;\n    // Best effort: a closed page has already dropped its session.\n    await session.send('Page.stopScreencast').catch(() => undefined);\n    await session.detach().catch(() => undefined);\n  }\n}\n","import { Hono } from 'hono';\nimport type { Container } from 'inversify';\n\nimport { PLAYWRIGHT_TYPES } from '../../../constants/playwright-types.js';\nimport type { BrowserInstance, IBrowserService } from '../../../services/BrowserService.js';\nimport type { IPageRegistry, PageEntry } from '../../../services/PageRegistry.js';\n\n/**\n * Browser data for API response\n */\ninterface BrowserResponse {\n  id: string;\n  profileName?: string;\n  currentPageId: string | null;\n  createdAt: string;\n  pages: PageResponse[];\n}\n\n/**\n * Page data for API response\n */\ninterface PageResponse {\n  id: string;\n  url: string;\n  title: string;\n  createdAt: string;\n}\n\n/**\n * Stats for API response\n */\ninterface StatsResponse {\n  totalBrowsers: number;\n  totalPages: number;\n}\n\n/**\n * Create API router for dashboard\n *\n * @param container - InversifyJS container for dependency injection\n * @returns Hono router with API endpoints\n */\nexport function createApiRouter(container: Container): Hono {\n  const app = new Hono();\n\n  /**\n   * GET /api/browsers - List all browsers with nested pages and stats\n   */\n  app.get('/browsers', async (c) => {\n    try {\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n      const pageRegistry = container.get<IPageRegistry>(PLAYWRIGHT_TYPES.PageRegistry);\n\n      const browserInstances = browserService.listBrowsers();\n      const allPages = pageRegistry.list();\n\n      // Build browser response with nested pages\n      const browsers: BrowserResponse[] = browserInstances.map((browser: BrowserInstance) => {\n        const browserPages = allPages.filter((p: PageEntry) => p.browserId === browser.id);\n\n        return {\n          id: browser.id,\n          profileName: browser.profileName,\n          currentPageId: browser.currentPageId,\n          createdAt: browser.createdAt.toISOString(),\n          pages: browserPages.map((page: PageEntry) => ({\n            id: page.id,\n            url: page.url,\n            title: page.title,\n            createdAt: page.createdAt.toISOString(),\n          })),\n        };\n      });\n\n      const stats: StatsResponse = {\n        totalBrowsers: browserInstances.length,\n        totalPages: allPages.length,\n      };\n\n      return c.json({ browsers, stats });\n    } catch (error) {\n      console.error('Failed to list browsers:', error);\n      return c.json(\n        {\n          error: 'Failed to list browsers',\n          message: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  /**\n   * DELETE /api/browsers - Close all browsers\n   */\n  app.delete('/browsers', async (c) => {\n    try {\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n      await browserService.closeAll();\n\n      return c.json({ success: true, message: 'All browsers closed' });\n    } catch (error) {\n      console.error('Failed to close all browsers:', error);\n      return c.json(\n        {\n          error: 'Failed to close all browsers',\n          message: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  /**\n   * DELETE /api/browsers/:id - Close a specific browser\n   */\n  app.delete('/browsers/:id', async (c) => {\n    try {\n      const browserId = c.req.param('id');\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n\n      const browser = browserService.getBrowser(browserId);\n      if (!browser) {\n        return c.json({ error: `Browser \"${browserId}\" not found` }, 404);\n      }\n\n      await browserService.closeBrowser(browserId);\n\n      return c.json({ success: true, message: `Browser \"${browserId}\" closed` });\n    } catch (error) {\n      console.error('Failed to close browser:', error);\n      return c.json(\n        {\n          error: 'Failed to close browser',\n          message: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  /**\n   * DELETE /api/pages/:id - Close a specific page\n   */\n  app.delete('/pages/:id', async (c) => {\n    try {\n      const pageId = c.req.param('id');\n      const pageRegistry = container.get<IPageRegistry>(PLAYWRIGHT_TYPES.PageRegistry);\n\n      const pageEntry = pageRegistry.get(pageId);\n      if (!pageEntry) {\n        return c.json({ error: `Page \"${pageId}\" not found` }, 404);\n      }\n\n      if (!pageEntry.page) {\n        return c.json({ error: `Page \"${pageId}\" is in extension mode and cannot be closed via API` }, 400);\n      }\n\n      // Close the page - this will trigger the registry cleanup via the 'close' event\n      await pageEntry.page.close();\n\n      return c.json({ success: true, message: `Page \"${pageId}\" closed` });\n    } catch (error) {\n      console.error('Failed to close page:', error);\n      return c.json(\n        {\n          error: 'Failed to close page',\n          message: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  return app;\n}\n","/**\n * Utility formatters for dashboard display\n */\n\n/**\n * Format a Date object to a human-readable string\n *\n * @param date - Date to format\n * @returns Formatted timestamp string\n */\nexport function formatTimestamp(date: Date): string {\n  return date.toLocaleString('en-US', {\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false,\n  });\n}\n\n/**\n * Format duration in milliseconds to human readable\n *\n * @param ms - Duration in milliseconds\n * @returns Formatted duration string\n */\nexport function formatDuration(ms: number): string {\n  const seconds = Math.floor(ms / 1000);\n  const minutes = Math.floor(seconds / 60);\n  const hours = Math.floor(minutes / 60);\n\n  if (hours > 0) {\n    return `${hours}h ${minutes % 60}m`;\n  }\n  if (minutes > 0) {\n    return `${minutes}m ${seconds % 60}s`;\n  }\n  return `${seconds}s`;\n}\n\n/**\n * Calculate duration since a given date\n *\n * @param date - Start date\n * @returns Duration string\n */\nexport function formatAge(date: Date): string {\n  const now = new Date();\n  const ms = now.getTime() - date.getTime();\n  return formatDuration(ms);\n}\n","// Plain CSS class names for SSR compatibility\nexport const container = 'dashboard-container';\nexport const header = 'dashboard-header';\nexport const controlsSection = 'controls-section';\nexport const button = 'btn';\nexport const buttonDanger = 'btn-danger';\nexport const tableContainer = 'table-container';\nexport const table = 'browser-table';\nexport const statsContainer = 'stats-container';\nexport const statCard = 'stat-card';\nexport const browserRow = 'browser-row';\nexport const pageRow = 'page-row';\nexport const statusActive = 'status-active';\nexport const statusInactive = 'status-inactive';\nexport const urlCell = 'url-cell';\nexport const timestampCell = 'timestamp-cell';\nexport const actionsCell = 'actions-cell';\nexport const killButton = 'kill-btn';\nexport const refreshButton = 'refresh-btn';\nexport const emptyState = 'empty-state';\n\n// Global styles as plain CSS string for SSR\nexport const globalStyles = `\n  * {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n  }\n\n  body {\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n    font-size: 14px;\n    line-height: 1.5;\n    color: #333;\n    background-color: #f5f5f5;\n  }\n\n  h1, h2, h3 {\n    margin-bottom: 1rem;\n  }\n\n  .dashboard-container {\n    max-width: 1400px;\n    margin: 0 auto;\n    padding: 2rem;\n  }\n\n  .dashboard-header {\n    background-color: #fff;\n    padding: 1.5rem;\n    margin-bottom: 1.5rem;\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n\n  .dashboard-header h1 {\n    margin-bottom: 0;\n  }\n\n  .controls-section {\n    background-color: #fff;\n    padding: 1rem;\n    margin-bottom: 1rem;\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n    display: flex;\n    gap: 1rem;\n    align-items: center;\n    flex-wrap: wrap;\n  }\n\n  .btn {\n    padding: 0.5rem 1rem;\n    border: none;\n    border-radius: 4px;\n    font-size: 14px;\n    cursor: pointer;\n    background-color: #2196f3;\n    color: white;\n    transition: background-color 0.2s;\n  }\n\n  .btn:hover {\n    background-color: #1976d2;\n  }\n\n  .btn:disabled {\n    background-color: #ccc;\n    cursor: not-allowed;\n  }\n\n  .btn-danger {\n    background-color: #f44336;\n  }\n\n  .btn-danger:hover {\n    background-color: #d32f2f;\n  }\n\n  .table-container {\n    background-color: #fff;\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n    overflow: hidden;\n  }\n\n  .browser-table {\n    width: 100%;\n    border-collapse: collapse;\n    table-layout: fixed;\n  }\n\n  .browser-table th {\n    background-color: #f8f9fa;\n    padding: 0.75rem;\n    text-align: left;\n    font-weight: 600;\n    border-bottom: 2px solid #dee2e6;\n    position: sticky;\n    top: 0;\n  }\n\n  .browser-table th:nth-child(1) {\n    width: 12%;\n  }\n\n  .browser-table th:nth-child(2) {\n    width: 10%;\n  }\n\n  .browser-table th:nth-child(3) {\n    width: 38%;\n  }\n\n  .browser-table th:nth-child(4) {\n    width: 15%;\n  }\n\n  .browser-table th:nth-child(5) {\n    width: 15%;\n  }\n\n  .browser-table th:nth-child(6) {\n    width: 10%;\n  }\n\n  .browser-table td {\n    padding: 0.75rem;\n    border-bottom: 1px solid #dee2e6;\n    vertical-align: middle;\n  }\n\n  .browser-row {\n    background-color: #e3f2fd;\n    font-weight: 500;\n  }\n\n  .browser-row:hover {\n    background-color: #bbdefb;\n  }\n\n  .page-row {\n    background-color: #fff;\n  }\n\n  .page-row:hover {\n    background-color: #f8f9fa;\n  }\n\n  .page-row td:first-child {\n    padding-left: 2rem;\n  }\n\n  .status-active {\n    color: #4caf50;\n    font-weight: 600;\n  }\n\n  .status-inactive {\n    color: #9e9e9e;\n  }\n\n  .url-cell {\n    max-width: 400px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-family: 'Courier New', monospace;\n    font-size: 13px;\n  }\n\n  .timestamp-cell {\n    white-space: nowrap;\n    font-size: 12px;\n    color: #666;\n  }\n\n  .actions-cell {\n    text-align: center;\n  }\n\n  .kill-btn {\n    padding: 0.25rem 0.5rem;\n    font-size: 12px;\n    background-color: #ff5722;\n    border: none;\n    border-radius: 4px;\n    color: white;\n    cursor: pointer;\n    transition: background-color 0.2s;\n  }\n\n  .kill-btn:hover {\n    background-color: #e64a19;\n  }\n\n  .refresh-btn {\n    background-color: #4caf50;\n  }\n\n  .refresh-btn:hover {\n    background-color: #388e3c;\n  }\n\n  .stats-container {\n    display: flex;\n    gap: 1.5rem;\n    margin-bottom: 1rem;\n  }\n\n  .stat-card {\n    background-color: #fff;\n    padding: 1rem;\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n    flex: 1;\n  }\n\n  .stat-card h3 {\n    font-size: 0.875rem;\n    color: #666;\n    margin-bottom: 0.5rem;\n  }\n\n  .stat-card .value {\n    font-size: 1.5rem;\n    font-weight: 700;\n    color: #333;\n  }\n\n  .empty-state {\n    padding: 3rem;\n    text-align: center;\n    color: #666;\n  }\n\n  .empty-state h3 {\n    margin-bottom: 0.5rem;\n    color: #999;\n  }\n`;\n","import { Fragment } from 'hono/jsx';\n\nimport { formatAge, formatTimestamp } from '../utils/formatters.js';\nimport * as styles from './styles.js';\n\n/**\n * Page data for display\n */\ninterface PageData {\n  id: string;\n  url: string;\n  title: string;\n  createdAt: Date;\n}\n\n/**\n * Browser data with nested pages\n */\ninterface BrowserData {\n  id: string;\n  profileName?: string;\n  currentPageId: string | null;\n  createdAt: Date;\n  pages: PageData[];\n}\n\n/**\n * BrowserTable component props\n */\ninterface BrowserTableProps {\n  browsers: BrowserData[];\n}\n\n/**\n * BrowserTable component\n *\n * Displays browsers and their pages in a hierarchical table\n */\nexport function BrowserTable({ browsers }: BrowserTableProps) {\n  if (browsers.length === 0) {\n    return (\n      <div class={styles.tableContainer}>\n        <div class={styles.emptyState}>\n          <h3>No Active Browsers</h3>\n          <p>Launch a browser using MCP tools to see it here.</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div class={styles.tableContainer}>\n      <table class={styles.table}>\n        <thead>\n          <tr>\n            <th>ID</th>\n            <th>Status</th>\n            <th>URL / Title</th>\n            <th>Created</th>\n            <th>Age</th>\n            <th>Actions</th>\n          </tr>\n        </thead>\n        <tbody id=\"browser-table-body\">\n          {browsers.map((browser) => (\n            <Fragment key={browser.id}>\n              <tr class={styles.browserRow}>\n                <td>{browser.id}</td>\n                <td>\n                  <span class={styles.statusActive}>\n                    {browser.pages.length} page{browser.pages.length !== 1 ? 's' : ''}\n                  </span>\n                </td>\n                <td>{browser.profileName || 'Default Profile'}</td>\n                <td class={styles.timestampCell}>{formatTimestamp(browser.createdAt)}</td>\n                <td class={styles.timestampCell}>{formatAge(browser.createdAt)}</td>\n                <td class={styles.actionsCell}>\n                  {/* Ids arrive from connecting extensions, so they are carried\n                      as data rather than spliced into an onclick script. */}\n                  <button type=\"button\" class={styles.killButton} data-browser-id={browser.id}>\n                    Kill\n                  </button>\n                </td>\n              </tr>\n              {browser.pages.map((page) => (\n                <tr key={page.id} class={styles.pageRow}>\n                  <td>\n                    {page.id}\n                    {browser.currentPageId === page.id && ' (active)'}\n                  </td>\n                  <td>\n                    <span class={styles.statusActive}>Open</span>\n                  </td>\n                  <td class={styles.urlCell} title={page.url}>\n                    {page.title || page.url || 'about:blank'}\n                  </td>\n                  <td class={styles.timestampCell}>{formatTimestamp(page.createdAt)}</td>\n                  <td class={styles.timestampCell}>{formatAge(page.createdAt)}</td>\n                  <td class={styles.actionsCell}>\n                    <button type=\"button\" class={styles.killButton} data-page-id={page.id}>\n                      Close\n                    </button>\n                  </td>\n                </tr>\n              ))}\n            </Fragment>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n","import * as styles from './styles.js';\n\n/**\n * Stats data structure\n */\ninterface Stats {\n  totalBrowsers: number;\n  totalPages: number;\n}\n\n/**\n * StatsHeader component props\n */\ninterface StatsHeaderProps {\n  stats: Stats;\n}\n\n/**\n * StatsHeader component\n *\n * Displays browser and page statistics\n */\nexport function StatsHeader({ stats }: StatsHeaderProps) {\n  return (\n    <div class={styles.statsContainer}>\n      <div class={styles.statCard}>\n        <h3>Active Browsers</h3>\n        <div class=\"value\">{stats.totalBrowsers}</div>\n      </div>\n      <div class={styles.statCard}>\n        <h3>Active Pages</h3>\n        <div class=\"value\">{stats.totalPages}</div>\n      </div>\n    </div>\n  );\n}\n","import { raw } from 'hono/html';\n\nimport { BrowserTable } from './BrowserTable.js';\nimport { StatsHeader } from './StatsHeader.js';\nimport * as styles from './styles.js';\n\n/**\n * Page data for display\n */\ninterface PageData {\n  id: string;\n  url: string;\n  title: string;\n  createdAt: Date;\n}\n\n/**\n * Browser data with nested pages\n */\ninterface BrowserData {\n  id: string;\n  profileName?: string;\n  currentPageId: string | null;\n  createdAt: Date;\n  pages: PageData[];\n}\n\n/**\n * Stats data structure\n */\ninterface Stats {\n  totalBrowsers: number;\n  totalPages: number;\n}\n\n/**\n * Dashboard component props\n */\ninterface DashboardProps {\n  browsers: BrowserData[];\n  stats: Stats;\n}\n\n/**\n * Dashboard component\n *\n * Main dashboard UI with auto-refresh polling and browser/page management\n */\nexport function Dashboard({ browsers, stats }: DashboardProps) {\n  return (\n    <div class={styles.container}>\n      <div class={styles.header}>\n        <div>\n          <h1>Playwright MCP Dashboard</h1>\n          <p>Browser automation management (auto-refresh every 3 seconds)</p>\n        </div>\n        <div>\n          <button\n            type=\"button\"\n            id=\"refresh-btn\"\n            class={`${styles.button} ${styles.refreshButton}`}\n            onclick=\"dashboard.manualRefresh()\"\n          >\n            Refresh Now\n          </button>\n          <button\n            type=\"button\"\n            id=\"kill-all-btn\"\n            class={`${styles.button} ${styles.buttonDanger}`}\n            onclick=\"dashboard.killAllBrowsers()\"\n          >\n            Kill All Browsers\n          </button>\n        </div>\n      </div>\n\n      <StatsHeader stats={stats} />\n\n      <BrowserTable browsers={browsers} />\n\n      <script>\n        {raw(`\nclass DashboardManager {\n  constructor() {\n    this.autoRefreshInterval = null;\n    this.isRefreshing = false;\n    this.startAutoRefresh();\n  }\n\n  startAutoRefresh() {\n    this.autoRefreshInterval = setInterval(() => {\n      this.fetchBrowsers();\n    }, 3000);\n  }\n\n  stopAutoRefresh() {\n    if (this.autoRefreshInterval) {\n      clearInterval(this.autoRefreshInterval);\n      this.autoRefreshInterval = null;\n    }\n  }\n\n  async fetchBrowsers() {\n    if (this.isRefreshing) return;\n    this.isRefreshing = true;\n\n    try {\n      const response = await fetch('/api/browsers');\n      const data = await response.json();\n      this.updateStats(data.stats);\n      this.updateBrowserTable(data.browsers);\n    } catch (error) {\n      console.error('Failed to fetch browsers:', error);\n    } finally {\n      this.isRefreshing = false;\n    }\n  }\n\n  updateStats(stats) {\n    const cards = document.querySelectorAll('.${styles.statCard} .value');\n    if (cards.length >= 2) {\n      cards[0].textContent = stats.totalBrowsers;\n      cards[1].textContent = stats.totalPages;\n    }\n  }\n\n  updateBrowserTable(browsers) {\n    const tbody = document.getElementById('browser-table-body');\n    if (!tbody) return;\n\n    if (browsers.length === 0) {\n      const container = tbody.closest('.${styles.tableContainer}');\n      if (container) {\n        container.innerHTML = \\`\n          <div class=\"${styles.emptyState}\">\n            <h3>No Active Browsers</h3>\n            <p>Launch a browser using MCP tools to see it here.</p>\n          </div>\n        \\`;\n      }\n      return;\n    }\n\n    // Rebuilt with DOM APIs, never innerHTML: browser and page ids come from\n    // whatever a connecting extension claimed, so treating them as markup would\n    // let a crafted id run script in this page's origin. textContent and\n    // dataset never reinterpret their input.\n    tbody.innerHTML = '';\n    browsers.forEach(browser => {\n      const browserRow = document.createElement('tr');\n      browserRow.className = '${styles.browserRow}';\n      this.appendTextCell(browserRow, browser.id);\n      this.appendBadgeCell(browserRow, browser.pages.length + ' page' + (browser.pages.length !== 1 ? 's' : ''));\n      this.appendTextCell(browserRow, browser.profileName || 'Default Profile');\n      this.appendTextCell(browserRow, this.formatTimestamp(browser.createdAt), '${styles.timestampCell}');\n      this.appendTextCell(browserRow, this.formatAge(browser.createdAt), '${styles.timestampCell}');\n      this.appendActionCell(browserRow, 'Kill', 'browserId', browser.id);\n      tbody.appendChild(browserRow);\n\n      browser.pages.forEach(page => {\n        const pageRow = document.createElement('tr');\n        pageRow.className = '${styles.pageRow}';\n        const isActive = browser.currentPageId === page.id;\n        this.appendTextCell(pageRow, page.id + (isActive ? ' (active)' : ''));\n        this.appendBadgeCell(pageRow, 'Open');\n        const urlCell = this.appendTextCell(\n          pageRow,\n          page.title || page.url || 'about:blank',\n          '${styles.urlCell}',\n        );\n        urlCell.title = page.url || '';\n        this.appendTextCell(pageRow, this.formatTimestamp(page.createdAt), '${styles.timestampCell}');\n        this.appendTextCell(pageRow, this.formatAge(page.createdAt), '${styles.timestampCell}');\n        this.appendActionCell(pageRow, 'Close', 'pageId', page.id);\n        tbody.appendChild(pageRow);\n      });\n    });\n  }\n\n  appendTextCell(row, text, className) {\n    const cell = document.createElement('td');\n    if (className) cell.className = className;\n    cell.textContent = text;\n    row.appendChild(cell);\n    return cell;\n  }\n\n  appendBadgeCell(row, text) {\n    const cell = document.createElement('td');\n    const badge = document.createElement('span');\n    badge.className = '${styles.statusActive}';\n    badge.textContent = text;\n    cell.appendChild(badge);\n    row.appendChild(cell);\n    return cell;\n  }\n\n  appendActionCell(row, label, datasetKey, id) {\n    const cell = document.createElement('td');\n    cell.className = '${styles.actionsCell}';\n    const button = document.createElement('button');\n    button.type = 'button';\n    button.className = '${styles.killButton}';\n    button.textContent = label;\n    // Carried as data, not as generated JS in an onclick attribute: the HTML\n    // parser decodes entities before the JS parser runs, so escaping alone\n    // cannot make an id safe to embed in a script.\n    button.dataset[datasetKey] = id;\n    cell.appendChild(button);\n    row.appendChild(cell);\n    return cell;\n  }\n\n  bindTableActions() {\n    const tbody = document.getElementById('browser-table-body');\n    if (!tbody) return;\n\n    tbody.addEventListener('click', event => {\n      const button = event.target.closest('button[data-browser-id], button[data-page-id]');\n      if (!button) return;\n\n      if (button.dataset.browserId) {\n        this.killBrowser(button.dataset.browserId);\n      } else if (button.dataset.pageId) {\n        this.killPage(button.dataset.pageId);\n      }\n    });\n  }\n\n  formatTimestamp(timestamp) {\n    const date = new Date(timestamp);\n    return date.toLocaleString('en-US', {\n      year: 'numeric',\n      month: '2-digit',\n      day: '2-digit',\n      hour: '2-digit',\n      minute: '2-digit',\n      second: '2-digit',\n      hour12: false,\n    });\n  }\n\n  formatAge(timestamp) {\n    const now = new Date();\n    const date = new Date(timestamp);\n    const ms = now.getTime() - date.getTime();\n    const seconds = Math.floor(ms / 1000);\n    const minutes = Math.floor(seconds / 60);\n    const hours = Math.floor(minutes / 60);\n\n    if (hours > 0) return \\`\\${hours}h \\${minutes % 60}m\\`;\n    if (minutes > 0) return \\`\\${minutes}m \\${seconds % 60}s\\`;\n    return \\`\\${seconds}s\\`;\n  }\n\n  escapeHtml(str) {\n    if (typeof str !== 'string') return str;\n    return str\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/\"/g, '&quot;')\n      .replace(/'/g, '&#039;');\n  }\n\n  manualRefresh() {\n    this.fetchBrowsers();\n  }\n\n  async killBrowser(browserId) {\n    try {\n      const response = await fetch(\\`/api/browsers/\\${browserId}\\`, { method: 'DELETE' });\n      if (response.ok) {\n        this.fetchBrowsers();\n      } else {\n        console.error('Failed to kill browser:', await response.text());\n      }\n    } catch (error) {\n      console.error('Failed to kill browser:', error);\n    }\n  }\n\n  async killPage(pageId) {\n    try {\n      const response = await fetch(\\`/api/pages/\\${pageId}\\`, { method: 'DELETE' });\n      if (response.ok) {\n        this.fetchBrowsers();\n      } else {\n        console.error('Failed to close page:', await response.text());\n      }\n    } catch (error) {\n      console.error('Failed to close page:', error);\n    }\n  }\n\n  async killAllBrowsers() {\n    if (!confirm('Are you sure you want to close all browsers?')) return;\n\n    try {\n      const response = await fetch('/api/browsers', { method: 'DELETE' });\n      if (response.ok) {\n        this.fetchBrowsers();\n      } else {\n        console.error('Failed to kill all browsers:', await response.text());\n      }\n    } catch (error) {\n      console.error('Failed to kill all browsers:', error);\n    }\n  }\n}\n\n// Initialize dashboard\nconst dashboard = new DashboardManager();\ndashboard.bindTableActions();\n\n// Cleanup on page unload\nwindow.addEventListener('beforeunload', () => {\n  dashboard.stopAutoRefresh();\n});\n        `)}\n      </script>\n    </div>\n  );\n}\n","import { raw } from 'hono/html';\n\nimport { globalStyles } from './styles.js';\n\n/**\n * Layout component props\n */\ninterface LayoutProps {\n  title: string;\n  children: unknown;\n}\n\n/**\n * Layout component\n *\n * Provides the HTML wrapper with styles and meta tags for SSR\n */\nexport function Layout({ title, children }: LayoutProps) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>{title}</title>\n        <style>{raw(globalStyles)}</style>\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n","import { Hono } from 'hono';\nimport type { Container } from 'inversify';\n\nimport { PLAYWRIGHT_TYPES } from '../../../constants/playwright-types.js';\nimport type { BrowserInstance, IBrowserService } from '../../../services/BrowserService.js';\nimport type { IPageRegistry, PageEntry } from '../../../services/PageRegistry.js';\nimport { Dashboard } from '../components/Dashboard.js';\nimport { Layout } from '../components/Layout.js';\n\n/**\n * Page data for display\n */\ninterface PageData {\n  id: string;\n  url: string;\n  title: string;\n  createdAt: Date;\n}\n\n/**\n * Browser data with nested pages\n */\ninterface BrowserData {\n  id: string;\n  profileName?: string;\n  currentPageId: string | null;\n  createdAt: Date;\n  pages: PageData[];\n}\n\n/**\n * Create dashboard router for server-side rendered UI\n *\n * @param container - InversifyJS container for dependency injection\n * @returns Hono router with dashboard routes\n */\nexport function createDashboardRouter(container: Container): Hono {\n  const app = new Hono();\n\n  /**\n   * GET / - Dashboard home page with SSR\n   *\n   * Renders the dashboard UI with initial server-side data\n   */\n  app.get('/', async (c) => {\n    try {\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n      const pageRegistry = container.get<IPageRegistry>(PLAYWRIGHT_TYPES.PageRegistry);\n\n      const browserInstances = browserService.listBrowsers();\n      const allPages = pageRegistry.list();\n\n      // Build browser data with nested pages\n      const browsers: BrowserData[] = browserInstances.map((browser: BrowserInstance) => {\n        const browserPages = allPages.filter((p: PageEntry) => p.browserId === browser.id);\n\n        return {\n          id: browser.id,\n          profileName: browser.profileName,\n          currentPageId: browser.currentPageId,\n          createdAt: browser.createdAt,\n          pages: browserPages.map((page: PageEntry) => ({\n            id: page.id,\n            url: page.url,\n            title: page.title,\n            createdAt: page.createdAt,\n          })),\n        };\n      });\n\n      const stats = {\n        totalBrowsers: browserInstances.length,\n        totalPages: allPages.length,\n      };\n\n      // Render dashboard with Layout wrapper\n      return c.html(\n        <Layout title=\"Playwright MCP Dashboard\">\n          <Dashboard browsers={browsers} stats={stats} />\n        </Layout>,\n      );\n    } catch (error) {\n      console.error('Failed to render dashboard:', error);\n\n      return c.html(\n        <Layout title=\"Playwright MCP Dashboard - Error\">\n          <div style=\"padding: 2rem; text-align: center;\">\n            <h1>Failed to load dashboard</h1>\n            <p>{error instanceof Error ? error.message : String(error)}</p>\n            <a href=\"/\" style=\"color: #2196f3; text-decoration: underline;\">\n              Retry\n            </a>\n          </div>\n        </Layout>,\n        500,\n      );\n    }\n  });\n\n  return app;\n}\n","/**\n * Origin / Host gate for the browser-automation daemon.\n *\n * The daemon listens on loopback, which makes it reachable by any page the user\n * happens to visit: a web page can `fetch` and open WebSockets to localhost, and\n * the daemon drives a real browser holding the user's logged-in sessions. Being\n * loopback-only is therefore not a security boundary on its own.\n *\n * Browsers attach an unforgeable `Origin` to cross-origin requests and to every\n * WebSocket handshake, and `Sec-Fetch-Site` to the no-Origin cases such as\n * `<script src>` and `<img>`. Requiring one of those to look local is what keeps\n * a visited page from driving the daemon, while a local process (the MCP proxy,\n * the CLI, curl) sends neither and is let through.\n */\n\nimport type { Context, MiddlewareHandler } from 'hono';\n\n/** How a request was classified, for handlers that need to know. */\nexport type RequestClass = 'local-process' | 'same-origin' | 'extension';\n\n/** Context key holding the classification for downstream handlers. */\nexport const REQUEST_CLASS_KEY = 'browseRequestClass';\n\n/** Env var listing extra hostnames the daemon may be addressed by. */\nexport const ALLOWED_HOSTS_ENV = 'BROWSE_TOOL_ALLOWED_HOSTS';\n\n/** Set to '1' to log rejections instead of blocking them. */\nexport const ORIGIN_GUARD_DISABLED_ENV = 'BROWSE_TOOL_DISABLE_ORIGIN_GUARD';\n\nconst LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0'];\n\nconst EXTENSION_ORIGIN_SCHEMES = ['chrome-extension://', 'moz-extension://', 'safari-web-extension://'];\n\nexport interface OriginGuardConfig {\n  /**\n   * Resolve the address the server is actually bound to. Called per request\n   * because the port is only known after the server binds.\n   */\n  resolveBinding: () => { host: string; port?: number };\n}\n\n/** Strip brackets and port so IPv6 and host:port forms compare equally. */\nfunction normalizeHostname(value: string): string {\n  const trimmed = value.trim().toLowerCase();\n\n  if (trimmed.startsWith('[')) {\n    const end = trimmed.indexOf(']');\n    return end === -1 ? trimmed : trimmed.slice(1, end);\n  }\n\n  const lastColon = trimmed.lastIndexOf(':');\n  if (lastColon !== -1 && !trimmed.slice(lastColon + 1).includes(':')) {\n    return trimmed.slice(0, lastColon);\n  }\n\n  return trimmed;\n}\n\nfunction parsePort(hostHeader: string): number | undefined {\n  const trimmed = hostHeader.trim();\n  const afterBracket = trimmed.startsWith('[') ? trimmed.slice(trimmed.indexOf(']') + 1) : trimmed;\n  const lastColon = afterBracket.lastIndexOf(':');\n  if (lastColon === -1) {\n    return undefined;\n  }\n\n  const port = Number(afterBracket.slice(lastColon + 1));\n  return Number.isInteger(port) ? port : undefined;\n}\n\nfunction allowedHostnames(boundHost: string): string[] {\n  const configured = (process.env[ALLOWED_HOSTS_ENV] ?? '')\n    .split(',')\n    .map((entry) => normalizeHostname(entry))\n    .filter((entry) => entry.length > 0);\n\n  return [...LOOPBACK_HOSTS.map(normalizeHostname), normalizeHostname(boundHost), ...configured];\n}\n\nfunction isExtensionOrigin(origin: string): boolean {\n  return EXTENSION_ORIGIN_SCHEMES.some((scheme) => origin.startsWith(scheme));\n}\n\n/** Whether an Origin header names this daemon itself. */\nexport function isSameOrigin(origin: string, boundHost: string, boundPort?: number): boolean {\n  let parsed: URL;\n  try {\n    parsed = new URL(origin);\n  } catch {\n    return false;\n  }\n\n  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n    return false;\n  }\n\n  if (!allowedHostnames(boundHost).includes(normalizeHostname(parsed.hostname))) {\n    return false;\n  }\n\n  const originPort = parsed.port ? Number(parsed.port) : undefined;\n  return boundPort === undefined || originPort === undefined || originPort === boundPort;\n}\n\n/** Whether a request may talk to the daemon, and as what kind of client. */\nexport function classifyRequest(\n  headers: { host?: string; origin?: string; secFetchSite?: string },\n  binding: { host: string; port?: number },\n): { allowed: true; requestClass: RequestClass } | { allowed: false; reason: string } {\n  // A forged Host is how DNS rebinding turns a public page into a local one, and\n  // it is also what the telemetry config echoes back to the extension.\n  if (headers.host !== undefined) {\n    const hostname = normalizeHostname(headers.host);\n    if (!allowedHostnames(binding.host).includes(hostname)) {\n      return { allowed: false, reason: `Host \"${headers.host}\" is not a local address for this daemon` };\n    }\n\n    const hostPort = parsePort(headers.host);\n    if (binding.port !== undefined && hostPort !== undefined && hostPort !== binding.port) {\n      return { allowed: false, reason: `Host port ${hostPort} does not match this daemon's port ${binding.port}` };\n    }\n  }\n\n  const origin = headers.origin;\n  if (origin !== undefined && origin !== 'null') {\n    if (isExtensionOrigin(origin)) {\n      return { allowed: true, requestClass: 'extension' };\n    }\n\n    if (isSameOrigin(origin, binding.host, binding.port)) {\n      return { allowed: true, requestClass: 'same-origin' };\n    }\n\n    return { allowed: false, reason: `Origin \"${origin}\" is not allowed to reach this daemon` };\n  }\n\n  // No Origin: either a local process, or a browser load that omits it such as\n  // `<script src>`. Sec-Fetch-Site is what separates those two.\n  const secFetchSite = headers.secFetchSite;\n  if (secFetchSite !== undefined && secFetchSite !== 'same-origin' && secFetchSite !== 'none') {\n    return { allowed: false, reason: `Cross-site request (Sec-Fetch-Site: ${secFetchSite}) is not allowed` };\n  }\n\n  return { allowed: true, requestClass: 'local-process' };\n}\n\n/**\n * Reject requests that a web page could have made.\n *\n * Mount before every other route, including the WebSocket upgrade — `@hono/node-ws`\n * routes upgrades through the same app, so a short-circuit here refuses the socket.\n */\nexport function createOriginGuard(config: OriginGuardConfig): MiddlewareHandler {\n  return async (c, next) => {\n    const binding = config.resolveBinding();\n    const verdict = classifyRequest(\n      {\n        host: c.req.header('host'),\n        origin: c.req.header('origin'),\n        secFetchSite: c.req.header('sec-fetch-site'),\n      },\n      binding,\n    );\n\n    if (!verdict.allowed) {\n      if (process.env[ORIGIN_GUARD_DISABLED_ENV] === '1') {\n        console.warn(`[OriginGuard] Allowing blocked request (guard disabled): ${verdict.reason}`);\n        c.set(REQUEST_CLASS_KEY, 'local-process');\n        return next();\n      }\n\n      console.warn(`[OriginGuard] Rejected ${c.req.method} ${c.req.path}: ${verdict.reason}`);\n      return c.json({ error: 'forbidden', reason: verdict.reason }, 403);\n    }\n\n    c.set(REQUEST_CLASS_KEY, verdict.requestClass);\n    return next();\n  };\n}\n\n/** Read the classification stored by {@link createOriginGuard}. */\nexport function getRequestClass(c: Context): RequestClass | undefined {\n  return c.get(REQUEST_CLASS_KEY) as RequestClass | undefined;\n}\n","/**\n * HTTP Server for Browser Automation\n *\n * DESIGN PATTERNS:\n * - Hono for HTTP routing and middleware\n * - Dependency injection with InversifyJS Container\n * - Service layer for business logic\n * - Generic execute endpoint for all tools\n *\n * CODING STANDARDS:\n * - Use Hono for HTTP routing and middleware\n * - Use CORS middleware for cross-origin requests\n * - Keep route handlers thin, delegate to services\n *\n * AVOID:\n * - Business logic in route handlers\n * - Missing error handling\n */\n\nimport { coerceArgs, formatZodError } from '@agimon-ai/foundation-validator';\nimport type { CallToolResult } from '@modelcontextprotocol/server';\nimport { SpanStatusCode } from '@opentelemetry/api';\nimport { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport type { Container } from 'inversify';\nimport { z } from 'zod';\n\nimport { PLAYWRIGHT_TYPES } from '../constants/playwright-types.js';\nimport type { BrowserInstance, IBrowserService } from '../services/BrowserService.js';\nimport { CustomToolService } from '../services/CustomToolService.js';\nimport type { ExtensionTaskQueue } from '../services/ExtensionTaskQueue.js';\nimport type { IPageRegistry, PageEntry } from '../services/PageRegistry.js';\nimport { SessionViewScreencast } from '../services/SessionViewScreencast.js';\nimport type { IStealthLauncher } from '../services/StealthLauncher.js';\nimport { type ITelemetryService, TelemetryService } from '../services/TelemetryService.js';\nimport type { Tool } from '../types/index.js';\nimport { getPlaywrightHost } from '../utils/networkConfig.js';\nimport { OWNER_HEADER, OUTPUT_DIRECTORY_HEADER, PROFILE_HEADER, runWithOwner } from '../utils/ownerContext.js';\nimport { getPackageVersion } from '../utils/packageVersion.js';\nimport { runWithTelemetryContext, TELEMETRY_HEADERS, telemetryContextFromHeaders } from '../utils/telemetryContext.js';\nimport { createApiRouter } from './dashboard/routes/api.js';\nimport { createDashboardRouter } from './dashboard/routes/dashboard.js';\nimport { createExtensionRoutes } from './extension-routes.js';\nimport { createOriginGuard, isSameOrigin } from './middleware/originGuard.js';\n\n/**\n * Execute request body\n */\ninterface ExecuteRequest {\n  tool: string;\n  arguments: Record<string, unknown>;\n}\n\n/**\n * Execute response body\n */\ninterface ExecuteResponse {\n  success: boolean;\n  result?: CallToolResult;\n  error?: string;\n}\n\nconst CALLER_VALIDATION_ERROR_TYPE = 'caller_validation';\n\ninterface CustomToolsResponse {\n  tools: Array<{\n    name: string;\n    description: string;\n    suggestionActions?: string;\n    inputSchema: Record<string, unknown>;\n    capabilities: Record<string, unknown>;\n  }>;\n  error?: string;\n}\n\n/**\n * Browser info for health response\n */\ninterface BrowserInfo {\n  id: string;\n  pageCount: number;\n  createdAt: string;\n}\n\n/**\n * Health response\n */\ninterface HealthResponse {\n  status: 'healthy' | 'unhealthy';\n  service: string;\n  serviceName: string;\n  /** Package version of the running daemon, so clients can detect version skew. */\n  version: string;\n  timestamp: string;\n  browsers: {\n    count: number;\n    instances: BrowserInfo[];\n  };\n}\n\nconst SESSION_VIEW_PREFIX = '/session-view/';\nconst SESSION_VIEW_OWNER_MAX_LENGTH = 128;\nconst SESSION_VIEW_OWNER_PATTERN = /^[A-Za-z0-9._:-]+$/;\nconst SESSION_VIEW_METADATA_TIMEOUT_MS = 500;\nconst SESSION_VIEW_JPEG_QUALITY = 60;\nconst SESSION_VIEW_SCREENSHOT_TIMEOUT_MS = 3000;\n/** One body for a missing page and another owner's page, so neither can be told apart. */\nconst SESSION_VIEW_PAGE_NOT_FOUND = { error: 'Page not found' };\nconst SESSION_VIEW_FRAME_CONTENT_TYPES = { jpeg: 'image/jpeg', png: 'image/png' } as const;\n/** Number of the returned JPEG frame; send it back as `?since=` to get 204 while it is still current. */\nconst SESSION_VIEW_FRAME_SEQ_HEADER = 'X-Frame-Seq';\ntype SessionViewFrameFormat = keyof typeof SESSION_VIEW_FRAME_CONTENT_TYPES;\n\ninterface SessionViewPage {\n  id: string;\n  url: string;\n  title: string;\n  /** Epoch ms of the last tool call against the page. */\n  lastAccessedAt: number;\n  current: boolean;\n}\n\ninterface SessionViewBrowser {\n  id: string;\n  profileName?: string;\n  headless: boolean;\n  /** Epoch ms when the browser went away without browse-tool closing it. */\n  crashedAt?: number;\n  pages: SessionViewPage[];\n}\n\n/** Returns the owner header when it is present and well formed, otherwise undefined. */\nfunction readSessionViewOwner(value: string | undefined): string | undefined {\n  if (!value || value.length > SESSION_VIEW_OWNER_MAX_LENGTH || !SESSION_VIEW_OWNER_PATTERN.test(value)) {\n    return undefined;\n  }\n  return value;\n}\n\n/** Origin plus path only: query strings and fragments can carry OAuth codes and tokens. */\nfunction toSessionViewUrl(rawUrl: string): string {\n  let parsed: URL;\n  try {\n    parsed = new URL(rawUrl);\n  } catch {\n    return '';\n  }\n  if (parsed.origin !== 'null') {\n    return `${parsed.origin}${parsed.pathname}`;\n  }\n  // about:blank keeps its path; data: and similar would put their payload there.\n  return parsed.protocol === 'about:' ? `${parsed.protocol}${parsed.pathname}` : parsed.protocol;\n}\n\n/** Refreshes url and title, giving up after a cap so a hung page cannot stall the listing. */\nasync function refreshPageMetadata(pageRegistry: IPageRegistry, pageId: string): Promise<void> {\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  const cap = new Promise<void>((resolve) => {\n    timer = setTimeout(resolve, SESSION_VIEW_METADATA_TIMEOUT_MS);\n  });\n  try {\n    await Promise.race([pageRegistry.updateMetadata(pageId), cap]);\n  } catch {\n    // Documented fallback: the listing shows the last known url and title.\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\n/**\n * Options for wiring the daemon's lifecycle into its HTTP surface.\n */\nexport interface HttpServerOptions {\n  /**\n   * Resolve the address the server is bound to, used to decide which origins\n   * count as local. Called per request because the port is assigned after the\n   * app is built.\n   */\n  resolveBinding?: () => { host: string; port?: number };\n  /**\n   * Invoked by `POST /shutdown`. When omitted the route reports 404, so a\n   * process that cannot shut itself down does not advertise that it can.\n   */\n  onShutdownRequested?: () => void;\n  /**\n   * Invoked for every request that does real work, so the daemon can tell an\n   * idle lifetime from a busy one. Health probes are excluded: discovery polls\n   * them on daemons it never goes on to use.\n   */\n  onRequestServed?: () => void;\n}\n\nfunction getResultError(result: CallToolResult): string | undefined {\n  if (!result.isError) {\n    return undefined;\n  }\n\n  const firstContent = result.content[0];\n  if (firstContent?.type === 'text' && typeof firstContent.text === 'string') {\n    return firstContent.text;\n  }\n\n  return 'Unknown tool execution error';\n}\n\ninterface StructuredBrowserError {\n  code: string;\n  retryable: boolean;\n  targetKey?: string;\n  targetState: string;\n  retryAfterMs: number;\n}\n\ntype ToolErrorCategory = 'caller_execution' | 'infrastructure_failure' | 'page_state' | 'spec_failure' | 'tool_failure';\n\ninterface StructuredToolOutcome {\n  category: string;\n  failedTests?: number;\n  passedTests?: number;\n  timings?: {\n    executionMs?: number;\n    hooksMs?: number;\n    totalMs?: number;\n  };\n}\n\nfunction getStructuredBrowserError(result: CallToolResult): StructuredBrowserError | undefined {\n  const structured = result.structuredContent;\n  const error = structured && typeof structured === 'object' && 'error' in structured ? structured.error : undefined;\n  if (\n    typeof error !== 'object' ||\n    error === null ||\n    !('code' in error) ||\n    typeof error.code !== 'string' ||\n    !('retryable' in error) ||\n    typeof error.retryable !== 'boolean' ||\n    !('targetState' in error) ||\n    typeof error.targetState !== 'string' ||\n    !('retryAfterMs' in error) ||\n    typeof error.retryAfterMs !== 'number'\n  ) {\n    return undefined;\n  }\n\n  return {\n    code: error.code,\n    retryable: error.retryable,\n    targetKey: 'targetKey' in error && typeof error.targetKey === 'string' ? error.targetKey : undefined,\n    targetState: error.targetState,\n    retryAfterMs: error.retryAfterMs,\n  };\n}\n\nfunction getStructuredToolOutcome(result: CallToolResult): StructuredToolOutcome | undefined {\n  const structured = result.structuredContent;\n  const outcome =\n    structured && typeof structured === 'object' && 'outcome' in structured ? structured.outcome : undefined;\n  if (typeof outcome !== 'object' || outcome === null || !('category' in outcome)) {\n    return undefined;\n  }\n\n  return outcome as StructuredToolOutcome;\n}\n\nfunction classifyToolError(toolName: string, result: CallToolResult, errorMessage: string): ToolErrorCategory {\n  const outcome = getStructuredToolOutcome(result);\n  if (outcome?.category === 'spec_failure') {\n    return 'spec_failure';\n  }\n  if (getStructuredBrowserError(result)) {\n    return 'infrastructure_failure';\n  }\n  if (/element not found|stale (?:element|reference)|page .* not found/i.test(errorMessage)) {\n    return 'page_state';\n  }\n  if (\n    (toolName === 'browser_evaluate_script' || toolName === 'browser_run_code') &&\n    /SyntaxError|Illegal return statement|Unexpected token/i.test(errorMessage)\n  ) {\n    return 'caller_execution';\n  }\n  return 'tool_failure';\n}\n\nfunction isOperationalToolFailure(category: ToolErrorCategory): boolean {\n  return category === 'infrastructure_failure' || category === 'tool_failure';\n}\n\nfunction resolveTelemetryService(container: Container): ITelemetryService {\n  try {\n    return container.get<ITelemetryService>(PLAYWRIGHT_TYPES.TelemetryService);\n  } catch {\n    return new TelemetryService();\n  }\n}\n\n/**\n * Refreshes idle timestamps after a tool ran against a page.\n * Stealth-mode browsers live in StealthLauncher's registry rather than\n * BrowserService's, so fall back to touching them there.\n */\nfunction recordPageActivity(container: Container, pageRegistry: IPageRegistry, pageId: string): void {\n  const pageEntry = pageRegistry.get(pageId);\n  if (!pageEntry) {\n    return;\n  }\n\n  const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n  if (browserService.recordBrowserActivity(pageEntry.browserId, pageId)) {\n    return;\n  }\n\n  try {\n    const stealthLauncher = container.get<IStealthLauncher>(PLAYWRIGHT_TYPES.StealthLauncher);\n    if (stealthLauncher.touch(pageEntry.browserId)) {\n      pageRegistry.touchPage(pageId);\n    }\n  } catch {\n    // StealthLauncher is not bound in every container (e.g. the MCP proxy container)\n  }\n}\n\nfunction normalizeBuiltInToolName(toolName: string): string {\n  if (toolName.startsWith('browser-') || toolName === 'run-spec' || toolName === 'discover-specs') {\n    return toolName.replaceAll('-', '_');\n  }\n\n  return toolName;\n}\n\nfunction getStaleToolReplacement(toolName: string): string | undefined {\n  const staleToolNames: Record<string, string> = {\n    browser_console_logs: 'browser_list_console_messages',\n    browser_console_messages: 'browser_list_console_messages',\n    browser_evaluate: 'browser_evaluate_script',\n    browser_get_console_logs: 'browser_list_console_messages',\n    browser_resize: 'browser_resize_page',\n    browser_take_screenshot: 'browser_screenshot',\n    browser_list_session: 'browser_list_pages',\n  };\n\n  return staleToolNames[toolName];\n}\n\n/**\n * Create HTTP server for browser automation\n *\n * @param container - InversifyJS container with services\n * @returns Configured Hono app\n */\nexport function createHttpServer(container: Container, options: HttpServerOptions = {}): Hono {\n  const app = new Hono();\n  // Flipped as soon as shutdown starts so discoverers stop reusing a dying daemon.\n  let shuttingDown = false;\n  const pageRegistry = container.get<import('../services/PageRegistry.js').IPageRegistry>(\n    PLAYWRIGHT_TYPES.PageRegistry,\n  );\n  const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n  const extensionTaskQueue = container.get<ExtensionTaskQueue>(PLAYWRIGHT_TYPES.ExtensionTaskQueue);\n  const telemetry = resolveTelemetryService(container);\n  const customToolService = new CustomToolService(pageRegistry, extensionTaskQueue, telemetry, browserService);\n\n  // First gate: refuse anything a web page could have sent. This runs before\n  // every route, including the WebSocket upgrade.\n  const resolveBinding: NonNullable<HttpServerOptions['resolveBinding']> =\n    options.resolveBinding ?? (() => ({ host: getPlaywrightHost() }));\n  app.use('*', createOriginGuard({ resolveBinding }));\n\n  // CORS reflects only origins the guard above would accept. Reflecting any\n  // origin with credentials is what let a visited page read daemon responses.\n  app.use(\n    '*',\n    cors({\n      origin: (origin) => {\n        if (!origin) {\n          return null;\n        }\n        const binding = resolveBinding();\n        const allowed =\n          origin.startsWith('chrome-extension://') ||\n          origin.startsWith('moz-extension://') ||\n          isSameOrigin(origin, binding.host, binding.port);\n        return allowed ? origin : null;\n      },\n      credentials: true,\n      allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],\n      allowHeaders: [\n        'Content-Type',\n        'Accept',\n        'Authorization',\n        OWNER_HEADER,\n        PROFILE_HEADER,\n        OUTPUT_DIRECTORY_HEADER,\n        ...TELEMETRY_HEADERS,\n      ],\n    }),\n  );\n\n  // Attribute every record this request produces to the calling agent, rather\n  // than to whichever agent's env this shared daemon happened to inherit.\n  // Wrapping as middleware covers handler bodies, error paths, and crash logs\n  // alike, since AsyncLocalStorage propagates across the awaited `next()`.\n  app.use('*', (c, next) =>\n    runWithTelemetryContext(\n      telemetryContextFromHeaders((name) => c.req.header(name)),\n      next,\n    ),\n  );\n\n  // Registered after the guards so a rejected request never counts as work.\n  const recordRequest = options.onRequestServed;\n  if (recordRequest) {\n    app.use('*', async (c, next) => {\n      // Session-view reads are polled by the Po hub; counting them would keep\n      // an otherwise idle daemon alive forever.\n      if (c.req.path !== '/health' && !c.req.path.startsWith(SESSION_VIEW_PREFIX)) {\n        recordRequest();\n      }\n      await next();\n    });\n  }\n\n  // Health check endpoint\n  app.get('/health', (c) => {\n    try {\n      if (shuttingDown) {\n        return c.json(\n          {\n            status: 'unhealthy',\n            service: 'browse-tool-http',\n            serviceName: 'browse-tool-http',\n            version: getPackageVersion(),\n            error: 'Server is shutting down',\n          },\n          503,\n        );\n      }\n\n      const browsers = browserService.listBrowsers();\n\n      const response: HealthResponse = {\n        status: 'healthy',\n        service: 'browse-tool-http',\n        serviceName: 'browse-tool-http',\n        version: getPackageVersion(),\n        timestamp: new Date().toISOString(),\n        browsers: {\n          count: browsers.length,\n          instances: browsers.map((b: BrowserInstance) => ({\n            id: b.id,\n            pageCount: b.pageIds.size,\n            createdAt: b.createdAt.toISOString(),\n          })),\n        },\n      };\n\n      return c.json(response);\n    } catch (error) {\n      return c.json(\n        {\n          status: 'unhealthy',\n          service: 'browse-tool-http',\n          serviceName: 'browse-tool-http',\n          version: getPackageVersion(),\n          error: error instanceof Error ? error.message : String(error),\n        },\n        503,\n      );\n    }\n  });\n\n  // Graceful shutdown endpoint. Letting the daemon close its own browsers is the\n  // only way to stop cleanly; signalling it from outside races browser teardown.\n  app.post('/shutdown', (c) => {\n    const requestShutdown = options.onShutdownRequested;\n    if (!requestShutdown) {\n      return c.json({ error: 'Shutdown endpoint is not available on this server' }, 404);\n    }\n\n    shuttingDown = true;\n    // Respond before tearing down, so the caller learns the request was accepted.\n    setImmediate(requestShutdown);\n    return c.json({ status: 'shutting-down' }, 202);\n  });\n\n  // Execute tool endpoint\n  app.post('/execute', async (c) => {\n    try {\n      const body = (await c.req.json()) as ExecuteRequest;\n\n      if (!body.tool) {\n        return c.json<ExecuteResponse>(\n          {\n            success: false,\n            error: 'Missing \"tool\" field in request body',\n          },\n          400,\n        );\n      }\n\n      const pageId = typeof body.arguments?.pageId === 'string' ? body.arguments.pageId : undefined;\n\n      return await telemetry.runInSpan(\n        'browse_tool.http.execute',\n        {\n          attributes: {\n            'http.method': 'POST',\n            'http.route': '/execute',\n            'browse_tool.tool.name': body.tool,\n            'browse_tool.page.id': pageId,\n          },\n        },\n        async (span) => {\n          const tools = container.getAll<Tool>(PLAYWRIGHT_TYPES.Tool);\n          const normalizedToolName = normalizeBuiltInToolName(body.tool);\n          const tool = tools.find((t) => t.getDefinition().name === normalizedToolName);\n\n          if (process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING === '1' && body.tool === 'browser_launch') {\n            const videoDir =\n              typeof body.arguments?.videoDir === 'string'\n                ? body.arguments.videoDir\n                : typeof body.arguments?.video_dir === 'string'\n                  ? body.arguments.video_dir\n                  : '';\n            telemetry.log('debug', 'browse-tool extension recording launch', {\n              attributes: {\n                'browse_tool.launch.mode': typeof body.arguments?.mode === 'string' ? body.arguments.mode : '',\n                'browse_tool.recording.video_dir': videoDir,\n              },\n            });\n          }\n\n          if (!tool) {\n            const replacement = getStaleToolReplacement(normalizedToolName);\n            const errorMessage = replacement\n              ? `Tool \"${body.tool}\" not found. Use \"${replacement}\" instead.`\n              : `Tool \"${body.tool}\" not found`;\n            span?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });\n            telemetry.log('warn', 'browse-tool HTTP execute rejected unknown tool', {\n              attributes: {\n                'http.route': '/execute',\n                'browse_tool.tool.name': body.tool,\n                'browse_tool.tool.normalized_name': normalizedToolName,\n                'browse_tool.tool.suggested_name': replacement,\n              },\n            });\n            return c.json<ExecuteResponse>(\n              {\n                success: false,\n                error: errorMessage,\n              },\n              404,\n            );\n          }\n\n          const rawArgs = body.arguments || {};\n          let parsedArgs: Record<string, unknown>;\n          try {\n            parsedArgs = tool.getInputSchema().parse(coerceArgs(rawArgs, tool.getInputSchema()));\n          } catch (error) {\n            if (error instanceof z.ZodError) {\n              const validationMessage = formatZodError(error, {\n                schemaName: normalizedToolName,\n                schema: tool.getInputSchema(),\n              });\n              const validationAttributes = {\n                'error.type': CALLER_VALIDATION_ERROR_TYPE,\n                'browse_tool.error.category': CALLER_VALIDATION_ERROR_TYPE,\n                'browse_tool.validation.failed': true,\n                'browse_tool.validation.issue_count': error.issues.length,\n              };\n              span?.setAttributes(validationAttributes);\n              telemetry.log('warn', 'browse-tool HTTP execute rejected invalid arguments', {\n                attributes: {\n                  'http.route': '/execute',\n                  'browse_tool.tool.name': normalizedToolName,\n                  'browse_tool.page.id': pageId,\n                  ...validationAttributes,\n                },\n              });\n              return c.json<ExecuteResponse>({\n                success: false,\n                result: {\n                  content: [{ type: 'text', text: validationMessage }],\n                  isError: true,\n                },\n                error: validationMessage,\n              });\n            }\n            throw error;\n          }\n\n          const args = parsedArgs;\n          const requestPageId = typeof args.pageId === 'string' ? args.pageId : undefined;\n\n          // Record activity before AND after the tool runs. A long-running tool\n          // would otherwise leave the browser looking idle for its whole\n          // duration and could be reaped mid-call.\n          if (requestPageId) {\n            recordPageActivity(container, pageRegistry, requestPageId);\n          }\n\n          let result: CallToolResult;\n          try {\n            result = await runWithOwner(\n              c.req.header(OWNER_HEADER),\n              () => tool.execute(parsedArgs),\n              c.req.header(PROFILE_HEADER),\n              c.req.header(OUTPUT_DIRECTORY_HEADER),\n            );\n          } catch (error) {\n            result = {\n              content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],\n              isError: true,\n            };\n          }\n\n          if (requestPageId) {\n            recordPageActivity(container, pageRegistry, requestPageId);\n          }\n\n          const errorMessage = getResultError(result);\n          if (errorMessage) {\n            const structuredError = getStructuredBrowserError(result);\n            const outcome = getStructuredToolOutcome(result);\n            const errorCategory = classifyToolError(normalizedToolName, result, errorMessage);\n            const operationalFailure = isOperationalToolFailure(errorCategory);\n            const errorAttributes = structuredError\n              ? {\n                  'browser.error.code': structuredError.code,\n                  'browser.error.retryable': structuredError.retryable,\n                  'browser.error.retry_after_ms': structuredError.retryAfterMs,\n                  'browser.target.key': structuredError.targetKey,\n                  'browser.target.state': structuredError.targetState,\n                }\n              : {};\n            const classificationAttributes = {\n              'browse_tool.error.category': errorCategory,\n              'tool.outcome.failed': true,\n              'tool.result.error': operationalFailure,\n              ...(outcome?.timings\n                ? {\n                    'browse_tool.spec.execution_ms': outcome.timings.executionMs,\n                    'browse_tool.spec.hooks_ms': outcome.timings.hooksMs,\n                    'browse_tool.spec.total_ms': outcome.timings.totalMs,\n                    'browse_tool.spec.failed_tests': outcome.failedTests,\n                    'browse_tool.spec.passed_tests': outcome.passedTests,\n                  }\n                : {}),\n            };\n            span?.setAttributes({ ...classificationAttributes, ...errorAttributes });\n            if (operationalFailure) {\n              span?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });\n            }\n            telemetry.log(\n              'warn',\n              operationalFailure\n                ? 'browse-tool HTTP execute returned tool error'\n                : 'browse-tool HTTP execute returned expected failed outcome',\n              {\n                attributes: {\n                  'http.route': '/execute',\n                  'browse_tool.tool.name': normalizedToolName,\n                  'browse_tool.page.id': requestPageId,\n                  'tool.name': normalizedToolName,\n                  'error.message': errorMessage,\n                  ...classificationAttributes,\n                  ...errorAttributes,\n                },\n              },\n            );\n          } else {\n            telemetry.log('debug', 'browse-tool HTTP execute succeeded', {\n              attributes: {\n                'http.route': '/execute',\n                'browse_tool.tool.name': normalizedToolName,\n                'browse_tool.page.id': requestPageId,\n              },\n            });\n          }\n\n          return c.json<ExecuteResponse>({\n            success: !result.isError,\n            result,\n            error: errorMessage,\n          });\n        },\n      );\n    } catch (error) {\n      telemetry.log('error', 'browse-tool HTTP execute request crashed', {\n        attributes: {\n          'http.route': '/execute',\n        },\n        exception: error,\n      });\n      return c.json<ExecuteResponse>(\n        {\n          success: false,\n          error: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  // List browsers endpoint\n  app.get('/browsers', (c) => {\n    try {\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n      const browsers = browserService.listBrowsers();\n\n      return c.json({\n        browsers: browsers.map((b: BrowserInstance) => ({\n          id: b.id,\n          profileName: b.profileName,\n          pageIds: Array.from(b.pageIds),\n          currentPageId: b.currentPageId,\n          createdAt: b.createdAt.toISOString(),\n        })),\n      });\n    } catch (error) {\n      return c.json(\n        {\n          error: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  // Session view: one owner's browsers and pages, for the Po hub. Fails closed:\n  // the owner header is required, only exact owner matches are listed, and\n  // reading never counts as page activity.\n  app.get('/session-view/browsers', async (c) => {\n    const ownerId = readSessionViewOwner(c.req.header(OWNER_HEADER));\n    if (!ownerId) {\n      return c.json({ error: `Missing or invalid ${OWNER_HEADER} header` }, 400);\n    }\n\n    const ownedPages = pageRegistry\n      .findByOwner(ownerId)\n      .filter((entry) => entry.ownerId === ownerId && entry.mode === 'playwright' && entry.page);\n    await Promise.all(ownedPages.map((entry) => refreshPageMetadata(pageRegistry, entry.id)));\n\n    const browsers = new Map<string, SessionViewBrowser>();\n    for (const entry of ownedPages) {\n      const instance = browserService.getBrowser(entry.browserId);\n      if (!instance) {\n        continue;\n      }\n      let view = browsers.get(instance.id);\n      if (!view) {\n        view = { id: instance.id, profileName: instance.profileName, headless: instance.headless ?? true, pages: [] };\n        browsers.set(instance.id, view);\n      }\n      view.pages.push({\n        id: entry.id,\n        url: toSessionViewUrl(entry.url),\n        title: entry.title,\n        lastAccessedAt: entry.lastAccessedAt.getTime(),\n        current: instance.currentPageId === entry.id,\n      });\n    }\n\n    for (const crash of browserService.listRecentCrashes()) {\n      if (!crash.ownerIds.includes(ownerId) || browserService.getBrowser(crash.browserId)) {\n        continue;\n      }\n      const existing = browsers.get(crash.browserId);\n      if (existing?.crashedAt !== undefined && existing.crashedAt >= crash.at) {\n        continue;\n      }\n      browsers.set(crash.browserId, {\n        id: crash.browserId,\n        profileName: crash.profileName,\n        headless: crash.headless ?? true,\n        crashedAt: crash.at,\n        pages: [],\n      });\n    }\n\n    return c.json({ browsers: Array.from(browsers.values()) });\n  });\n\n  // One capture per page and format; concurrent callers share it.\n  const inFlightFrames = new Map<string, Promise<Buffer>>();\n  const screencast = new SessionViewScreencast({ quality: SESSION_VIEW_JPEG_QUALITY });\n  const captureFrame = (\n    entry: PageEntry & { page: NonNullable<PageEntry['page']> },\n    format: SessionViewFrameFormat,\n  ) => {\n    const key = `${entry.id}:${format}`;\n    const pending = inFlightFrames.get(key);\n    if (pending) {\n      return pending;\n    }\n    const capture = entry.page\n      .screenshot({\n        type: format,\n        ...(format === 'jpeg' ? { quality: SESSION_VIEW_JPEG_QUALITY } : {}),\n        scale: 'css',\n        timeout: SESSION_VIEW_SCREENSHOT_TIMEOUT_MS,\n      })\n      .finally(() => inFlightFrames.delete(key));\n    inFlightFrames.set(key, capture);\n    return capture;\n  };\n\n  app.get('/session-view/pages/:pageId/frame', async (c) => {\n    const ownerId = readSessionViewOwner(c.req.header(OWNER_HEADER));\n    if (!ownerId) {\n      return c.json({ error: `Missing or invalid ${OWNER_HEADER} header` }, 400);\n    }\n    const format = c.req.query('format') ?? 'jpeg';\n    if (format !== 'jpeg' && format !== 'png') {\n      return c.json({ error: 'format must be jpeg or png' }, 400);\n    }\n\n    const pageId = c.req.param('pageId');\n    const entry = pageRegistry.get(pageId);\n    if (!entry || entry.ownerId !== ownerId) {\n      return c.json(SESSION_VIEW_PAGE_NOT_FOUND, 404);\n    }\n    const page = entry.page;\n    if (entry.mode !== 'playwright' || !page) {\n      return c.json({ error: 'Frames are not available for extension-mode pages' }, 409);\n    }\n\n    try {\n      if (format === 'png') {\n        const frame = await captureFrame({ ...entry, page }, format);\n        return c.body(new Uint8Array(frame), 200, {\n          'Content-Type': SESSION_VIEW_FRAME_CONTENT_TYPES[format],\n          'Cache-Control': 'no-store',\n        });\n      }\n      const frame = await screencast.frame(pageId, page, () => captureFrame({ ...entry, page }, format));\n      const seq = String(frame.seq);\n      // The viewer already holds this frame: answer with its number only.\n      if (c.req.query('since') === seq) {\n        return c.body(null, 204, { [SESSION_VIEW_FRAME_SEQ_HEADER]: seq, 'Cache-Control': 'no-store' });\n      }\n      return c.body(new Uint8Array(frame.data), 200, {\n        'Content-Type': SESSION_VIEW_FRAME_CONTENT_TYPES[format],\n        'Cache-Control': 'no-store',\n        [SESSION_VIEW_FRAME_SEQ_HEADER]: seq,\n      });\n    } catch (error) {\n      const errorName = error instanceof Error ? error.name : 'Error';\n      // Page id and error name only: messages can quote the page URL.\n      telemetry.log('warn', 'browse-tool session-view frame capture failed', {\n        attributes: {\n          'http.route': '/session-view/pages/:pageId/frame',\n          'browse_tool.page.id': pageId,\n          'error.type': errorName,\n        },\n      });\n      if (!pageRegistry.get(pageId) || page.isClosed()) {\n        return c.json(SESSION_VIEW_PAGE_NOT_FOUND, 404);\n      }\n      if (errorName === 'TimeoutError') {\n        return c.json({ error: 'Frame capture timed out' }, 504);\n      }\n      return c.json({ error: 'Frame capture failed' }, 500);\n    }\n  });\n\n  // List tools endpoint\n  app.get('/tools', (c) => {\n    try {\n      const tools = container.getAll<Tool>(PLAYWRIGHT_TYPES.Tool);\n\n      return c.json({\n        tools: tools.map((t) => t.getDefinition()),\n      });\n    } catch (error) {\n      return c.json(\n        {\n          error: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  app.get('/custom-tools', async (c) => {\n    const directory = c.req.query('dir');\n\n    if (!directory) {\n      return c.json<CustomToolsResponse>({ tools: [], error: 'Missing \"dir\" query parameter' }, 400);\n    }\n\n    try {\n      const tools = await customToolService.listTools(directory);\n      return c.json<CustomToolsResponse>({ tools });\n    } catch (error) {\n      return c.json<CustomToolsResponse>(\n        {\n          tools: [],\n          error: error instanceof Error ? error.message : String(error),\n        },\n        400,\n      );\n    }\n  });\n\n  app.post('/custom-tools', async (c) => {\n    const directory = c.req.query('dir');\n\n    if (!directory) {\n      return c.json<ExecuteResponse>({ success: false, error: 'Missing \"dir\" query parameter' }, 400);\n    }\n\n    try {\n      const body = (await c.req.json()) as ExecuteRequest;\n\n      if (!body.tool) {\n        return c.json<ExecuteResponse>({ success: false, error: 'Missing \"tool\" field in request body' }, 400);\n      }\n\n      const pageId = typeof body.arguments?.pageId === 'string' ? body.arguments.pageId : undefined;\n\n      return await telemetry.runInSpan(\n        'browse_tool.http.custom_tool.execute',\n        {\n          attributes: {\n            'http.method': 'POST',\n            'http.route': '/custom-tools',\n            'browse_tool.tool.name': body.tool,\n            'browse_tool.page.id': pageId,\n            'browse_tool.custom_tools.directory': directory,\n          },\n        },\n        async (span) => {\n          // Mirror /execute: a throwing tool is a tool error the agent can act\n          // on, not a transport crash. An ownership refusal is the common case.\n          let result: CallToolResult;\n          if (pageId) {\n            recordPageActivity(container, pageRegistry, pageId);\n          }\n\n          try {\n            result = await runWithOwner(\n              c.req.header(OWNER_HEADER),\n              () => customToolService.executeTool(directory, body.tool, body.arguments || {}),\n              c.req.header(PROFILE_HEADER),\n              c.req.header(OUTPUT_DIRECTORY_HEADER),\n            );\n          } catch (error) {\n            result = {\n              content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],\n              isError: true,\n            };\n          }\n\n          if (pageId) {\n            recordPageActivity(container, pageRegistry, pageId);\n          }\n\n          const errorMessage = getResultError(result);\n          if (errorMessage) {\n            span?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });\n            telemetry.log('warn', 'browse-tool custom tool execution returned tool error', {\n              attributes: {\n                'http.route': '/custom-tools',\n                'browse_tool.tool.name': body.tool,\n                'browse_tool.page.id': pageId,\n                'tool.result.error': true,\n                'tool.name': body.tool,\n                'error.message': errorMessage,\n              },\n            });\n          } else {\n            telemetry.log('debug', 'browse-tool custom tool execution succeeded', {\n              attributes: {\n                'http.route': '/custom-tools',\n                'browse_tool.tool.name': body.tool,\n                'browse_tool.page.id': pageId,\n              },\n            });\n          }\n\n          return c.json<ExecuteResponse>({\n            success: !result.isError,\n            result,\n            error: errorMessage,\n          });\n        },\n      );\n    } catch (error) {\n      telemetry.log('error', 'browse-tool custom tool request crashed', {\n        attributes: {\n          'http.route': '/custom-tools',\n          'browse_tool.custom_tools.directory': directory,\n        },\n        exception: error,\n      });\n      return c.json<ExecuteResponse>(\n        {\n          success: false,\n          error: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n\n  // Mount extension routes for Chrome extension communication\n  const extensionRouter = createExtensionRoutes(container);\n  app.route('/extension', extensionRouter);\n\n  // Mount dashboard API routes\n  const apiRouter = createApiRouter(container);\n  app.route('/api', apiRouter);\n\n  // Mount dashboard UI routes (SSR)\n  const dashboardRouter = createDashboardRouter(container);\n  app.route('/', dashboardRouter);\n\n  return app;\n}\n","/**\n * WebSocket Routes for Extension Communication\n *\n * DESIGN PATTERNS:\n * - WebSocket upgrade handler using @hono/node-ws\n * - Dependency injection with InversifyJS Container\n * - Pub/sub pattern via WebSocketHub\n *\n * CODING STANDARDS:\n * - Use Hono WebSocket helpers for upgrade\n * - Keep route handlers thin, delegate to WebSocketHub\n * - Handle connection lifecycle properly\n *\n * AVOID:\n * - Business logic in route handlers\n * - Missing error handling\n * - Memory leaks from uncleaned connections\n */\n\nimport type { Hono } from 'hono';\nimport type { UpgradeWebSocket } from 'hono/ws';\nimport type { Container } from 'inversify';\n\nimport { PLAYWRIGHT_TYPES } from '../constants/playwright-types.js';\nimport type { WebSocketHub } from '../services/WebSocketHub.js';\n\n/**\n * Setup WebSocket routes for extension communication\n *\n * @param app - Hono app instance\n * @param container - InversifyJS container with services\n * @param upgradeWebSocket - WebSocket upgrade function from @hono/node-ws\n */\nexport function setupWebSocketRoutes(app: Hono, container: Container, upgradeWebSocket: UpgradeWebSocket): void {\n  /**\n   * WebSocket endpoint for extension connections\n   * Path: /ws/extension/:browserId\n   */\n  app.get(\n    '/ws/extension/:browserId',\n    upgradeWebSocket((c) => {\n      const browserId = c.req.param('browserId');\n      let connectionId: string | null = null;\n\n      return {\n        onOpen(_event, ws) {\n          if (!browserId) {\n            ws.close(1008, 'Missing browserId');\n            return;\n          }\n          try {\n            const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n            connectionId = wsHub.addConnection(ws, browserId);\n          } catch {\n            ws.close(1011, 'Internal server error');\n          }\n        },\n\n        async onMessage(event) {\n          if (!connectionId) {\n            return;\n          }\n\n          try {\n            const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n            const data =\n              typeof event.data === 'string'\n                ? event.data\n                : new TextDecoder().decode(event.data instanceof Blob ? await event.data.arrayBuffer() : event.data);\n            wsHub.handleMessage(connectionId, data);\n          } catch {\n            // Error handling delegated to WebSocketHub\n          }\n        },\n\n        onClose() {\n          if (connectionId) {\n            try {\n              const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n              wsHub.removeConnection(connectionId);\n            } catch {\n              // Ignore cleanup errors\n            }\n          }\n        },\n\n        onError() {\n          if (connectionId) {\n            try {\n              const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n              wsHub.removeConnection(connectionId);\n            } catch {\n              // Ignore cleanup errors\n            }\n          }\n        },\n      };\n    }),\n  );\n\n  /**\n   * GET /ws/stats\n   * Get WebSocket connection statistics\n   */\n  app.get('/ws/stats', (c) => {\n    try {\n      const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n      const stats = wsHub.getStats();\n\n      return c.json({\n        totalConnections: stats.totalConnections,\n        browserCount: stats.browserCount,\n        connections: stats.connections.map((conn) => ({\n          id: conn.id,\n          browserId: conn.browserId,\n          connectedAt: conn.connectedAt.toISOString(),\n        })),\n      });\n    } catch (error) {\n      return c.json(\n        {\n          error: error instanceof Error ? error.message : String(error),\n        },\n        500,\n      );\n    }\n  });\n}\n","/**\n * HTTP Serve Command\n *\n * DESIGN PATTERNS:\n * - Command pattern with Commander for CLI argument parsing\n * - Async/await pattern for asynchronous operations\n * - Error handling pattern with try-catch and proper exit codes\n * - Dependency injection with InversifyJS Container\n * - Graceful shutdown pattern for SIGINT/SIGTERM\n *\n * CODING STANDARDS:\n * - Use async action handlers for asynchronous operations\n * - Provide clear option descriptions and default values\n * - Handle errors gracefully with process.exit()\n * - Log progress and errors to console\n * - Use Commander's .option() for inputs\n * - Implement graceful shutdown for server cleanup\n *\n * AVOID:\n * - Synchronous blocking operations in action handlers\n * - Missing error handling (always use try-catch)\n * - Hardcoded values (use options or environment variables)\n * - Not exiting with appropriate exit codes on errors\n * - Missing signal handlers for graceful shutdown\n */\n\nimport path from 'node:path';\n\nimport { DEFAULT_PORT_RANGE, PortRegistryService } from '@agimon-ai/foundation-port-registry';\nimport {\n  createProcessLease,\n  type ProcessLease,\n  ProcessRegistryService,\n  resolveSiblingRegistryPath,\n} from '@agimon-ai/foundation-process-registry';\nimport { serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { Command } from 'commander';\n\nimport { getCommandConfig, resolveConfiguredOption } from '../config.js';\nimport { PLAYWRIGHT_TYPES } from '../constants/playwright-types.js';\nimport { createHttpContainer } from '../container/index.js';\nimport { createHttpServer } from '../server/http.js';\nimport { setupWebSocketRoutes } from '../server/websocket-routes.js';\nimport type { BrowserProcessRegistry } from '../services/BrowserProcessRegistry.js';\nimport type { IBrowserService } from '../services/BrowserService.js';\nimport type { IExtensionSessionRegistry } from '../services/ExtensionSessionRegistry.js';\nimport type { ExtensionTaskQueue } from '../services/ExtensionTaskQueue.js';\nimport { CLAIM_TOKEN_ENV } from '../services/HttpServerManager.js';\nimport { BROWSER_IDLE_TIMEOUT_MINUTES_ENV_VAR, type IIdleCleanupService } from '../services/IdleCleanupService.js';\nimport { PROXY_CONFIG_DIR_ENV_VAR } from '../services/ProxyConfigService.js';\nimport type { IStealthLauncher } from '../services/StealthLauncher.js';\nimport type { IWebServerManager } from '../services/WebServerManager.js';\nimport type { WebSocketHub } from '../services/WebSocketHub.js';\nimport { buildPlaywrightBaseUrl, DEFAULT_MCP_PORT, getPlaywrightHost } from '../utils/networkConfig.js';\nimport { getPackageVersion } from '../utils/packageVersion.js';\nimport { resolveSelfProcessIdentity } from '../utils/processIdentity.js';\nimport { cleanupOrphanedBrowserTempDirs } from '../utils/tempDirCleanup.js';\nimport { BROWSE_TOOL_WORKSPACE_ROOT_ENV, resolveWorkspaceRoot, setWorkspaceRootEnv } from '../utils/workspaceRoot.js';\n\ninterface RegisterSelfOptions {\n  portRegistry: PortRegistryService;\n  repositoryPath: string;\n  serviceName: string;\n  environment: string;\n  requestedPort: number;\n  host: string;\n  metadata: Record<string, unknown>;\n}\n\n/** Whether a browse-tool daemon is already answering on this port. */\nasync function isDaemonServing(host: string, port: number): Promise<boolean> {\n  try {\n    const response = await fetch(`${buildPlaywrightBaseUrl(host, port)}/health`, {\n      signal: AbortSignal.timeout(2_000),\n    });\n    if (!response.ok) {\n      return false;\n    }\n    const body = (await response.json()) as { status?: string; service?: string };\n    return body.status === 'healthy' && body.service === 'browse-tool-http';\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Take ownership of this daemon's registry entry.\n *\n * When spawned by a client, the port was already claimed on our behalf and we\n * only confirm it. Losing that claim means another daemon now owns the scope, so\n * we exit rather than registering over it. When started by hand there is no\n * claim, and we refuse to displace a daemon that is already serving.\n *\n * @returns The port this daemon owns\n */\nasync function registerSelf(options: RegisterSelfOptions): Promise<number> {\n  const { portRegistry, repositoryPath, serviceName, environment, requestedPort, host, metadata } = options;\n  const scope = { repositoryPath, serviceName, serviceType: 'tool' as const, environment };\n  const claimToken = process.env[CLAIM_TOKEN_ENV];\n\n  if (claimToken) {\n    const confirmed = await portRegistry.confirmPort({\n      ...scope,\n      claimToken,\n      pid: process.pid,\n      metadata: { ...metadata, healthCheckUrl: `${buildPlaywrightBaseUrl(host, requestedPort)}/health` },\n    });\n\n    if (!confirmed.success || confirmed.port === undefined) {\n      console.error(`CLAIM_LOST: port ${requestedPort} is no longer ours (${confirmed.error}). Exiting.`);\n      process.exit(1);\n    }\n\n    return confirmed.port;\n  }\n\n  const existing = await portRegistry.getPort(scope);\n  const existingPort = existing.record?.port;\n  if (existingPort !== undefined && (await isDaemonServing(host, existingPort))) {\n    console.error(\n      `A browse-tool daemon is already running on port ${existingPort}. ` +\n        'Use it, or run `browse-tool stop` before starting another.',\n    );\n    process.exit(1);\n  }\n\n  const reserved = await portRegistry.reservePort({\n    ...scope,\n    preferredPort: requestedPort,\n    pid: process.pid,\n    host,\n    portRange: DEFAULT_PORT_RANGE,\n    metadata,\n  });\n\n  if (!reserved.success || reserved.port === undefined) {\n    throw new Error(\n      reserved.error || `Failed to reserve port in range ${DEFAULT_PORT_RANGE.min}-${DEFAULT_PORT_RANGE.max}`,\n    );\n  }\n\n  return reserved.port;\n}\n\ninterface HttpServeOptions {\n  port: string;\n  headless: boolean;\n  idleTimeout: string;\n  host: string;\n  registryDir?: string;\n  registryPath?: string;\n  pidsDir?: string;\n  profilesDir?: string;\n  snippetsDir?: string;\n  proxyConfigDir?: string;\n  workspaceRoot?: string;\n}\n\n/**\n * Start HTTP server for browser automation\n */\nexport const httpServeCommand = new Command('http-serve')\n  .description('Start HTTP server for browser automation')\n  .option('-p, --port <port>', 'Port to listen on', String(DEFAULT_MCP_PORT))\n  .option('--host <host>', 'Host to bind', getPlaywrightHost())\n  .option('--headless', 'Run browsers in headless mode by default', true)\n  .option('--no-headless', 'Run browsers in headed mode (visible window)')\n  .option('--idle-timeout <minutes>', 'Idle timeout in minutes before closing browsers', '30')\n  .option('--registry-dir <path>', 'Custom registry path or directory for service discovery')\n  .option('--registry-path <path>', 'Custom registry path or directory for service discovery')\n  .option('--pids-dir <path>', 'Custom PIDs directory for process tracking (deprecated)')\n  .option('--profiles-dir <path>', 'Custom profiles directory for browser profiles')\n  .option('--snippets-dir <path>', 'Directory used by browser_run_code to save and load reusable snippets')\n  .option('--proxy-config-dir <path>', 'Custom proxy config directory')\n  .option(\n    '--workspace-root <path>',\n    `Canonical workspace root for registry scoping (${BROWSE_TOOL_WORKSPACE_ROOT_ENV})`,\n  )\n  .action(async function (this: Command, options: HttpServeOptions) {\n    const commandDefaults = getCommandConfig<{\n      port?: number;\n      headless?: boolean;\n      idleTimeout?: number;\n      host?: string;\n      registryDir?: string;\n      registryPath?: string;\n      pidsDir?: string;\n      profilesDir?: string;\n      snippetsDir?: string;\n      proxyConfigDir?: string;\n      workspaceRoot?: string;\n    }>('httpServe');\n    const resolvedOptions: HttpServeOptions = {\n      port: resolveConfiguredOption(\n        this,\n        'port',\n        options.port,\n        commandDefaults.port !== undefined ? String(commandDefaults.port) : undefined,\n        process.env.PLAYWRIGHT_PORT,\n      ),\n      headless: resolveConfiguredOption(this, 'headless', options.headless, commandDefaults.headless),\n      idleTimeout: resolveConfiguredOption(\n        this,\n        'idleTimeout',\n        options.idleTimeout,\n        commandDefaults.idleTimeout !== undefined ? String(commandDefaults.idleTimeout) : undefined,\n        process.env[BROWSER_IDLE_TIMEOUT_MINUTES_ENV_VAR],\n      ),\n      host: resolveConfiguredOption(this, 'host', options.host, commandDefaults.host, process.env.PLAYWRIGHT_HOST),\n      registryDir: resolveConfiguredOption(\n        this,\n        'registryDir',\n        options.registryDir,\n        commandDefaults.registryDir,\n        process.env.PLAYWRIGHT_REGISTRY_DIR,\n      ),\n      registryPath: resolveConfiguredOption(\n        this,\n        'registryPath',\n        options.registryPath,\n        commandDefaults.registryPath,\n        process.env.PLAYWRIGHT_REGISTRY_PATH ?? process.env.PORT_REGISTRY_PATH,\n      ),\n      pidsDir: resolveConfiguredOption(\n        this,\n        'pidsDir',\n        options.pidsDir,\n        commandDefaults.pidsDir,\n        process.env.PLAYWRIGHT_PIDS_DIR,\n      ),\n      profilesDir: resolveConfiguredOption(\n        this,\n        'profilesDir',\n        options.profilesDir,\n        commandDefaults.profilesDir,\n        process.env.PLAYWRIGHT_PROFILES_DIR,\n      ),\n      snippetsDir: resolveConfiguredOption(\n        this,\n        'snippetsDir',\n        options.snippetsDir,\n        commandDefaults.snippetsDir,\n        process.env.BROWSE_TOOL_SNIPPETS_DIR,\n      ),\n      proxyConfigDir: resolveConfiguredOption(\n        this,\n        'proxyConfigDir',\n        options.proxyConfigDir,\n        commandDefaults.proxyConfigDir,\n        process.env[PROXY_CONFIG_DIR_ENV_VAR],\n      ),\n      workspaceRoot: resolveConfiguredOption(\n        this,\n        'workspaceRoot',\n        options.workspaceRoot,\n        commandDefaults.workspaceRoot,\n        process.env[BROWSE_TOOL_WORKSPACE_ROOT_ENV],\n      ),\n    };\n    let processLease: ProcessLease | undefined;\n\n    try {\n      const requestedPort = Number.parseInt(resolvedOptions.port, 10);\n      const serviceName = 'browse-tool-http';\n      const environment = process.env.NODE_ENV || 'development';\n      const repositoryPath = setWorkspaceRootEnv(\n        resolveWorkspaceRoot({\n          startPath: process.cwd(),\n          env: {\n            ...process.env,\n            [BROWSE_TOOL_WORKSPACE_ROOT_ENV]: resolvedOptions.workspaceRoot,\n          },\n        }),\n      );\n\n      // Set environment variables from CLI options\n      const registryPath =\n        resolvedOptions.registryPath || resolvedOptions.registryDir || process.env.PLAYWRIGHT_REGISTRY_PATH;\n      if (registryPath) {\n        process.env.PLAYWRIGHT_REGISTRY_DIR = registryPath;\n        process.env.PORT_REGISTRY_PATH = registryPath;\n        process.env.PROCESS_REGISTRY_PATH = resolveSiblingRegistryPath(registryPath, 'processes.json')!;\n      }\n      process.env.PLAYWRIGHT_HOST = resolvedOptions.host;\n      process.env.PLAYWRIGHT_PORT = resolvedOptions.port;\n      process.env[BROWSER_IDLE_TIMEOUT_MINUTES_ENV_VAR] = resolvedOptions.idleTimeout;\n\n      if (resolvedOptions.pidsDir) {\n        process.env.PLAYWRIGHT_PIDS_DIR = resolvedOptions.pidsDir;\n      }\n      if (resolvedOptions.profilesDir) {\n        process.env.PLAYWRIGHT_PROFILES_DIR = resolvedOptions.profilesDir;\n      }\n      if (resolvedOptions.snippetsDir) {\n        process.env.BROWSE_TOOL_SNIPPETS_DIR = path.resolve(resolvedOptions.snippetsDir);\n      }\n      if (resolvedOptions.proxyConfigDir) {\n        process.env[PROXY_CONFIG_DIR_ENV_VAR] = path.resolve(resolvedOptions.proxyConfigDir);\n      }\n\n      console.log('Starting HTTP server for browser automation...');\n      console.log(`   Port: ${requestedPort}`);\n      console.log(`   Host: ${resolvedOptions.host}`);\n      console.log(`   Headless: ${resolvedOptions.headless}`);\n      console.log(`   Idle Timeout: ${resolvedOptions.idleTimeout} minutes`);\n      if (process.env.BROWSE_TOOL_SNIPPETS_DIR) {\n        console.log(`   Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);\n      }\n      if (process.env[PROXY_CONFIG_DIR_ENV_VAR]) {\n        console.log(`   Proxy Config Dir: ${process.env[PROXY_CONFIG_DIR_ENV_VAR]}`);\n      }\n\n      // Create InversifyJS container with full HTTP server services\n      const container = createHttpContainer();\n      const processRegistry = container.get<ProcessRegistryService>(PLAYWRIGHT_TYPES.ProcessRegistryService);\n      const browserProcessRegistry = container.get<BrowserProcessRegistry>(PLAYWRIGHT_TYPES.BrowserProcessRegistry);\n\n      // Filled in once the shutdown routine below exists; the endpoint is wired\n      // up front so it is live from the moment the server accepts requests.\n      const shutdownRequest: { invoke?: () => void } = {};\n      const idleShutdownRequest: { invoke?: () => void } = {};\n\n      // Resolved lazily: the port is only settled once the registry hands it over.\n      const binding: { port?: number } = {};\n\n      // Start idle cleanup service\n      const idleCleanupService = container.get<IIdleCleanupService>(PLAYWRIGHT_TYPES.IdleCleanupService);\n      idleCleanupService.start(() => idleShutdownRequest.invoke?.());\n\n      // Create HTTP server\n      const app = createHttpServer(container, {\n        onShutdownRequested: () => shutdownRequest.invoke?.(),\n        onRequestServed: () => idleCleanupService.recordServerActivity(),\n        resolveBinding: () => ({ host: resolvedOptions.host, port: binding.port }),\n      });\n\n      // Setup WebSocket support for extension communication\n      const nodeWebSocket = createNodeWebSocket({ app });\n      const injectWebSocket = nodeWebSocket.injectWebSocket.bind(nodeWebSocket);\n      const upgradeWebSocket = nodeWebSocket.upgradeWebSocket;\n      setupWebSocketRoutes(app, container, upgradeWebSocket);\n\n      // Configure WebSocketHub event handlers for extension communication\n      const wsHub = container.get<WebSocketHub>(PLAYWRIGHT_TYPES.WebSocketHub);\n      const taskQueue = container.get<ExtensionTaskQueue>(PLAYWRIGHT_TYPES.ExtensionTaskQueue);\n      const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n      const sessionRegistry = container.get<IExtensionSessionRegistry>(PLAYWRIGHT_TYPES.ExtensionSessionRegistry);\n      const pageRegistry = container.get<import('../services/PageRegistry.js').IPageRegistry>(\n        PLAYWRIGHT_TYPES.PageRegistry,\n      );\n\n      wsHub.setEventHandlers({\n        onSessionRegister: (connection, message) => {\n          if (message.type === 'session:register') {\n            const requestedBrowserId = message.payload.browserId || connection.browserId;\n            const existingBrowser = browserService.getBrowser(requestedBrowserId);\n\n            let browserId: string;\n            let pageId: string;\n\n            if (existingBrowser && (existingBrowser.mode === 'extension' || existingBrowser.mode === 'vm')) {\n              browserId = existingBrowser.id;\n              const existingPages = pageRegistry.findByBrowser(browserId);\n              pageId = existingBrowser.currentPageId || existingPages[0]?.id || '';\n\n              wsHub.reassignConnection(connection.id, browserId);\n\n              if (!pageId) {\n                pageId = pageRegistry.registerExtensionPage(browserId);\n                existingBrowser.pageIds.add(pageId);\n                existingBrowser.currentPageId = pageId;\n              }\n\n              console.log(`[WebSocket] Extension connected to existing browser: ${browserId}, pageId: ${pageId}`);\n            } else {\n              browserId = requestedBrowserId;\n              browserService.registerExtensionBrowserWithId(browserId);\n              pageId = pageRegistry.registerExtensionPage(browserId);\n\n              const browserInstance = browserService.getBrowser(browserId);\n              if (browserInstance) {\n                browserInstance.pageIds.add(pageId);\n                browserInstance.currentPageId = pageId;\n              }\n\n              console.log(`[WebSocket] Extension connected with new browser: ${browserId}, pageId: ${pageId}`);\n            }\n\n            const pageEntry = pageRegistry.get(pageId);\n            if (pageEntry) {\n              pageEntry.extensionTabId = message.payload.tabId;\n              pageEntry.url = message.payload.url ?? pageEntry.url;\n            }\n\n            const session = sessionRegistry.register({\n              browserId,\n              tabId: message.payload.tabId,\n              url: message.payload.url,\n              metadata: {\n                transport: 'websocket',\n              },\n            });\n            wsHub.sendSessionAck(connection, message.id ?? '', session.id, session.controlMode);\n\n            wsHub.broadcastPageCreated(browserId, pageId, pageRegistry.get(pageId)?.url);\n          }\n        },\n        onTaskResult: (_connection, message) => {\n          if (message.type === 'task:result') {\n            const task = taskQueue.submitResult({\n              taskId: message.payload.taskId,\n              success: message.payload.success,\n              result: message.payload.result,\n              error: message.payload.error,\n            });\n\n            if (task?.browserId) {\n              browserService.recordBrowserActivity(task.browserId, task.pageId);\n            }\n          }\n        },\n        onTabMapped: (_connection, message) => {\n          if (message.type === 'tab:mapped') {\n            const pageEntry = pageRegistry.get(message.payload.pageId);\n            if (!pageEntry) {\n              return;\n            }\n\n            pageEntry.extensionTabId = message.payload.tabId;\n            browserService.recordBrowserActivity(pageEntry.browserId, pageEntry.id);\n          }\n        },\n        onDisconnect: (connection) => {\n          if (connection.sessionId) {\n            sessionRegistry.removeSession(connection.sessionId);\n          }\n          console.log(`[WebSocket] Extension disconnected: ${connection.browserId}`);\n\n          // A reconnect leaves another live connection, so only fail the browser's\n          // work once nothing can deliver it. Otherwise callers wait out the full\n          // task timeout for a result that can never arrive.\n          if (!wsHub.hasConnection(connection.browserId)) {\n            taskQueue.failPendingTasksForBrowser(\n              connection.browserId,\n              `Extension for browser \"${connection.browserId}\" disconnected`,\n            );\n          }\n        },\n      });\n\n      // Sockets can die without a close frame (laptop suspend, crashed\n      // extension); this drops them so pushes fall back to HTTP polling.\n      wsHub.startLivenessSweep();\n\n      const portRegistry = new PortRegistryService(process.env.PORT_REGISTRY_PATH);\n\n      if (registryPath) {\n        console.log(`   Registry path: ${registryPath}`);\n      } else {\n        console.log('   Registry path: default (~/.port-registry/ports.json)');\n      }\n\n      const finalPort = await registerSelf({\n        portRegistry,\n        repositoryPath,\n        serviceName,\n        environment,\n        requestedPort,\n        host: resolvedOptions.host,\n        metadata: {\n          healthCheckUrl: `${buildPlaywrightBaseUrl(resolvedOptions.host, requestedPort)}/health`,\n          headless: resolvedOptions.headless,\n          idleTimeout: resolvedOptions.idleTimeout,\n          version: getPackageVersion(),\n        },\n      });\n\n      binding.port = finalPort;\n\n      // Start server with WebSocket support\n      const server = serve({\n        fetch: app.fetch,\n        port: finalPort,\n        hostname: resolvedOptions.host,\n      });\n\n      // Inject WebSocket handler into the server\n      injectWebSocket(server);\n\n      await browserProcessRegistry.reconcileOrphanedBrowsers({\n        currentServerPid: process.pid,\n        currentPort: finalPort,\n        reconnectGraceMs: 0,\n      });\n\n      // Sweep temp directories left by daemons that were killed before they\n      // could clean up. Fire-and-forget: startup must not wait on disk I/O.\n      void cleanupOrphanedBrowserTempDirs().catch((error) => {\n        console.error('Temp directory cleanup error:', error);\n      });\n\n      // Rows for daemons that died without releasing anything accumulate forever\n      // and later read as live services. A start is the natural point to sweep\n      // them, and like the temp sweep it must not delay serving.\n      void processRegistry.cleanupStaleProcesses().catch((error) => {\n        console.error('Process registry cleanup error:', error);\n      });\n      void portRegistry.cleanupStaleAllocations().catch((error) => {\n        console.error('Port registry cleanup error:', error);\n      });\n\n      // Chrome and its helpers are children of this daemon, so the registry needs\n      // to know it can signal the whole group; releasing the bare pid orphans them.\n      const processIdentity = await resolveSelfProcessIdentity();\n\n      processLease = await createProcessLease(\n        {\n          repositoryPath,\n          serviceName,\n          serviceType: 'service',\n          environment,\n          pid: process.pid,\n          port: finalPort,\n          host: resolvedOptions.host,\n          processStartedAt: processIdentity.processStartedAt,\n          processGroupId: processIdentity.processGroupId,\n          terminationMode: processIdentity.terminationMode,\n          metadata: {\n            healthCheckUrl: `${buildPlaywrightBaseUrl(resolvedOptions.host, finalPort)}/health`,\n            headless: resolvedOptions.headless,\n            idleTimeout: resolvedOptions.idleTimeout,\n            transport: 'http',\n          },\n        },\n        processRegistry,\n      );\n\n      const baseUrl = buildPlaywrightBaseUrl(resolvedOptions.host, finalPort);\n      console.log(`HTTP server listening on ${baseUrl}`);\n      console.log(`  Health check: ${baseUrl}/health`);\n      console.log(`  Execute tool: POST ${baseUrl}/execute`);\n\n      console.log('\\nPress Ctrl+C to stop the server');\n\n      // Shutdown runs at most once: the first reason wins and later callers\n      // await the same teardown instead of racing a second one.\n      let shutdownPromise: Promise<void> | undefined;\n\n      const doShutdown = async (reason: string, exitCode: number): Promise<void> => {\n        console.log(`\\n\\n${reason} received. Shutting down gracefully...`);\n\n        try {\n          idleCleanupService.stop();\n          wsHub.stopLivenessSweep();\n\n          server.close();\n          console.log('Server closed');\n\n          // Close all browsers\n          try {\n            const browserService = container.get<IBrowserService>(PLAYWRIGHT_TYPES.BrowserService);\n            await browserService.closeAll();\n            console.log('All browsers closed');\n          } catch {\n            // Browser service may not have browsers\n          }\n\n          // Stealth browsers live in their own registry, so close them separately\n          try {\n            const stealthLauncher = container.get<IStealthLauncher>(PLAYWRIGHT_TYPES.StealthLauncher);\n            await stealthLauncher.closeAll();\n            console.log('All stealth browsers closed');\n          } catch {\n            // StealthLauncher may not be bound or may have no browsers\n          }\n\n          // Dev servers started for specs are children of this process, and its\n          // 'exit' handler cannot wait on them, so stop them while we still can.\n          try {\n            const webServerManager = container.get<IWebServerManager>(PLAYWRIGHT_TYPES.WebServerManager);\n            await webServerManager.stopServer();\n            console.log('Dev server stopped');\n          } catch (webServerError) {\n            console.error('Dev server cleanup error:', webServerError);\n          }\n\n          // Deregister service\n          try {\n            if (processLease) {\n              await processLease.release({ kill: false });\n            }\n            console.log('Service deregistered');\n          } catch (processReleaseError) {\n            console.error('Process registry cleanup error:', processReleaseError);\n          }\n\n          // Release our port row. Without this a clean exit leaves a row behind\n          // pointing at a dead pid, which later looks like a stale daemon.\n          try {\n            await portRegistry.releasePorts({\n              repositoryPath,\n              serviceName,\n              serviceType: 'tool',\n              environment,\n              pid: process.pid,\n            });\n          } catch (portReleaseError) {\n            console.error('Port registry cleanup error:', portReleaseError);\n          }\n\n          console.log('Goodbye!');\n          process.exit(exitCode);\n        } catch (error) {\n          console.error('Error during shutdown:', error);\n          process.exit(1);\n        }\n      };\n\n      const shutdown = (reason: string, exitCode = 0): Promise<void> => {\n        shutdownPromise ??= doShutdown(reason, exitCode);\n        return shutdownPromise;\n      };\n\n      shutdownRequest.invoke = () => void shutdown('Shutdown request', 0);\n      idleShutdownRequest.invoke = () => void shutdown('Server idle timeout', 0);\n\n      // A bind failure arrives as an 'error' event; without a handler it becomes\n      // an uncaught exception and the daemon dies still holding its registry row.\n      server.on('error', (error: NodeJS.ErrnoException) => {\n        if (error.code === 'EADDRINUSE') {\n          console.error(`PORT_IN_USE: port ${finalPort} is already bound by another process`);\n        } else {\n          console.error('HTTP server error:', error);\n        }\n        void shutdown('Server error', 1);\n      });\n\n      // Register signal handlers\n      process.on('SIGINT', () => void shutdown('SIGINT'));\n      process.on('SIGTERM', () => void shutdown('SIGTERM'));\n      process.on('SIGHUP', () => void shutdown('SIGHUP'));\n\n      process.on('uncaughtException', (error) => {\n        console.error('Uncaught exception:', error);\n        void shutdown('Uncaught exception', 1);\n      });\n      process.on('unhandledRejection', (reason) => {\n        console.error('Unhandled rejection:', reason);\n        void shutdown('Unhandled rejection', 1);\n      });\n\n      // Keep the detached child process alive even when no TTY/stdin is attached.\n      // Without this, the spawned bootstrap process can exit before health checks run.\n      await new Promise<never>(() => {});\n    } catch (error) {\n      if (processLease) {\n        try {\n          await processLease.release({ kill: false });\n        } catch (cleanupError) {\n          console.error('Process registry cleanup error:', cleanupError);\n        }\n      }\n      console.error('Error starting HTTP server:', error);\n      process.exit(1);\n    }\n  });\n"],"mappings":"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"}