# Hira SDK — AI Context File (Complete Reference)
# Version: 1.0.6
# Last updated: 2026-03-10
#
# This file is the "brain" for AI code generation.
# Feed it to Cursor / ChatGPT / Claude / Copilot and describe
# what flow you want — the AI will generate correct code.

================================================================================
1. IMPORTS
================================================================================

```ts
import {
  AntidetectBaseFlow,   // Base class for flows
  AntidetectProvider,   // Enum: GPM, HIDEMIUM, GENLOGIN, ADSPOWER
  BrowserUtils,         // Browser interaction helper (click, type, goto, etc.)
  FlowLogger,           // Logger instance
  IScriptContext,       // Type for script() context parameter
  defineFlowConfig,     // Helper to define type-safe config
} from '@hira-core/sdk'
```

================================================================================
2. AntidetectProvider
================================================================================

```ts
enum AntidetectProvider {
  GPM = 'gpm',           // GPM Login  — default API: http://127.0.0.1:19995
  HIDEMIUM = 'hidemium', // Hidemium   — default API: http://127.0.0.1:2222
  GENLOGIN = 'genlogin', // Coming soon
  ADSPOWER = 'adspower', // Coming soon
}
```

================================================================================
3. defineFlowConfig()
================================================================================

Declares input fields, profile fields, and output schema.
TypeScript enforces unique keys, labels, and indexes at compile time.

```ts
const config = defineFlowConfig({
  globalInput: [
    // Shared across all profiles — user fills once
    { key: 'targetUrl', label: 'Target URL', type: 'text', required: true },
    { key: 'delay', label: 'Delay (ms)', type: 'number', defaultValue: 3000 },
    {
      key: 'theme',
      label: 'Theme',
      type: 'select',
      options: [
        { label: 'Light', value: 'light' },
        { label: 'Dark', value: 'dark' },
      ],
      defaultValue: 'light',
    },
    { key: 'notes', label: 'Notes', type: 'textarea' },
  ],

  profileInput: [
    // Per-profile data — each profile has its own values
    { key: 'username', label: 'Username', type: 'text', required: true },
    { key: 'password', label: 'Password', type: 'text', required: true },
  ],

  output: [
    // Results per profile — written by writeOutput()
    { index: 0, key: 'status', label: 'Login Status' },
    { index: 1, key: 'title', label: 'Page Title' },
  ],
})
```

Input field types: 'text' | 'number' | 'boolean' | 'select' | 'textarea'
- 'text'     → string
- 'number'   → number
- 'boolean'  → boolean
- 'select'   → string (must have options[])
- 'textarea' → string

Validation rules (enforced at compile time):
- All keys must be unique within each array
- All labels must be unique within each array
- Output indexes must be unique
- Select options labels and values must be unique per field

================================================================================
4. FLOW CLASS STRUCTURE
================================================================================

```ts
type Config = typeof config

export class MyFlow extends AntidetectBaseFlow<Config> {
  constructor(provider: AntidetectProvider = AntidetectProvider.GPM) {
    super(provider, new FlowLogger('MyFlow'), config)
  }

  async script(context: IScriptContext<Config>): Promise<void> {
    const browser = new BrowserUtils(context)
    // Your automation logic here
  }
}
```

Entry point file (index.ts):
```ts
export { MyFlow as default } from './flow'
```

IMPORTANT: Entry file MUST use `export default`.

================================================================================
5. IScriptContext — What script() receives
================================================================================

```ts
interface IScriptContext<TConfig> {
  browser: Browser              // Puppeteer Browser instance
  page: Page                    // Puppeteer Page (default tab)
  profile: IAntidetectProfile   // Current profile info
  index: number                 // Profile index (0-based)
  globalInput: { ... }          // Type-safe from config.globalInput
  profileInput: { ... }         // Type-safe from config.profileInput
  output: { ... }               // Previous output values (null if not yet written)
  logger: ILogger               // Logger
}
```

IAntidetectProfile:
```ts
interface IAntidetectProfile {
  id: string
  name: string
  provider: 'gpm' | 'hidemium' | 'genlogin'
  raw_proxy?: string
  browser_type: 'chromium' | 'firefox'
  browser_version: string
  group_id?: string
  profile_path: string
  note: string
  created_at: string  // ISO 8601
}
```

ILogger methods:
- logger.info(message)
- logger.warn(message)
- logger.error(message)
- logger.debug(message)
- logger.success(message)

================================================================================
6. BrowserUtils — COMPLETE METHOD REFERENCE
================================================================================

Create: `const browser = new BrowserUtils(context)`

All methods respect abort signals — if flow is cancelled, they throw immediately.

────────────────────────────────────────────────────────────────────────────────
6.1  sleep(ms: number): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Pause execution for ms milliseconds. Respects abort signal.

Log: ⏳ Sleep 2000ms

Example:
```ts
await browser.sleep(2000)
```

────────────────────────────────────────────────────────────────────────────────
6.2  waitForElement(selector, timeout?, scope?): Promise<ElementHandle | null>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  selector: string    — CSS selector or XPath (auto-detected)
  timeout:  number    — default 10000ms
  scope:    Frame     — optional iframe scope

Returns: ElementHandle if found, null if not found. Does NOT throw.

XPath auto-detection: if selector starts with '//' or '(' → treated as XPath.

Log: 🔍 Waiting: #selector
Log: ⚠️ Element not found: #selector (timeout: 10000ms)  (when not found)

Example:
```ts
const el = await browser.waitForElement('#dashboard', 15000)
if (el) { /* found */ }

// XPath:
const btn = await browser.waitForElement('//button[text()="Submit"]')

// In iframe:
const frame = context.page.frames().find(f => f.url().includes('captcha'))
const el = await browser.waitForElement('#checkbox', 10000, frame)
```

────────────────────────────────────────────────────────────────────────────────
6.3  click(target, options?): Promise<boolean>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  target:  string | ElementHandle  — CSS/XPath selector OR element reference
  options: {
    delay?:   number  — delay before clicking (ms)
    timeout?: number  — waitForElement timeout (default 10000)
    frame?:   Frame   — click inside iframe
  }

Returns: true if clicked, false if element not found. Does NOT throw.

Behavior: waits for element → scrolls into view → clicks.

Log: 🖱️ Click: #submit-btn
Log: ❌ Click failed — element not found: #submit-btn  (when not found)

Example:
```ts
const ok = await browser.click('#submit-btn')
if (!ok) { /* button not found */ }

// XPath:
await browser.click('//button[text()="Login"]')

// With delay + timeout:
await browser.click('.slow-btn', { timeout: 20000, delay: 500 })

// ElementHandle:
const el = await browser.waitForElement('.my-btn')
if (el) await browser.click(el)

// In iframe:
const frame = context.page.frames().find(f => f.name() === 'payment')
await browser.click('#pay-now', { frame })
```

────────────────────────────────────────────────────────────────────────────────
6.4  type(selector, text, options?): Promise<boolean>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  selector: string  — CSS/XPath selector
  text:     string  — text to type
  options: {
    delay?: number  — delay between keystrokes (default 50ms)
    frame?: Frame   — type inside iframe
  }

Returns: true if typed, false if element not found. Does NOT throw.

IMPORTANT: ALWAYS clears existing content first (triple-click select all → Backspace → type).
Long text (>20 chars) is truncated in log output.

Log: ⌨️ Type "user@exam..." → #email
Log: ❌ Type failed — element not found: #email  (when not found)

Example:
```ts
await browser.type('#email', 'user@example.com')
await browser.type('#password', 'secret123')

// Slow typing:
await browser.type('#search', 'query', { delay: 100 })

// In iframe:
const frame = context.page.frames().find(f => f.name() === 'editor')
await browser.type('.text-area', 'Hello', { frame })
```

────────────────────────────────────────────────────────────────────────────────
6.5  getText(selector, frame?): Promise<string | null>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  selector: string  — CSS/XPath selector
  frame:    Frame   — optional iframe scope

Returns: trimmed text content, or null if not found. Does NOT throw.

Log: 📄 getText: h1

Example:
```ts
const title = await browser.getText('h1')
const balance = await browser.getText('.balance span')
if (balance) {
  await browser.writeOutput('balance', balance)
}
```

────────────────────────────────────────────────────────────────────────────────
6.6  exists(selector, timeout?, frame?): Promise<boolean>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  selector: string  — CSS/XPath selector
  timeout:  number  — default 3000ms (SHORTER than waitForElement's 10000ms)
  frame:    Frame   — optional iframe scope

Returns: true if element exists, false if not.

Example:
```ts
const isLoggedIn = await browser.exists('#dashboard', 5000)
if (isLoggedIn) {
  // skip login
}

// With iframe:
const frame = context.page.frames().find(f => f.url().includes('recaptcha'))
const hasCaptcha = await browser.exists('#recaptcha-anchor', 5000, frame)
```

────────────────────────────────────────────────────────────────────────────────
6.7  goto(url, options?): Promise<boolean>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  url:     string  — URL to navigate to
  options: {
    waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
    // default: 'domcontentloaded'
  }

Returns: true if navigation succeeded, false on error. Does NOT throw.
Timeout: 30 seconds (hardcoded).

Log: 🌐 Navigate → https://example.com
Log: ❌ Navigate failed: URL — error message  (on error)

Example:
```ts
const ok = await browser.goto('https://example.com')
if (!ok) { /* navigation failed */ }

// Wait for full load:
await browser.goto('https://dashboard.com', { waitUntil: 'networkidle0' })
```

────────────────────────────────────────────────────────────────────────────────
6.8  waitForNavigation(options?): Promise<boolean>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  options: {
    timeout?:   number  — default 30000ms
    waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
    // default: 'domcontentloaded'
  }

Returns: true if navigation completed, false on timeout. Does NOT throw.

Log: 🔄 Waiting for navigation...
Log: ⚠️ Navigation timeout  (on timeout)

Example:
```ts
await browser.click('#login-btn')
const ok = await browser.waitForNavigation()
if (!ok) { /* timeout */ }

// With options:
await browser.click('a.next')
await browser.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 })
```

────────────────────────────────────────────────────────────────────────────────
6.9  screenshot(path?): Promise<unknown>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  path: string  — optional file path to save screenshot

Returns: base64 string if no path, saves to file if path provided, null on error.

Log: 📸 Screenshot: ./debug.png
Log: ❌ Screenshot failed — error message  (on error)

Example:
```ts
await browser.screenshot('./debug.png')

// On error:
const ok = await browser.click('#missing')
if (!ok) await browser.screenshot('error.png')
```

────────────────────────────────────────────────────────────────────────────────
6.10  switchToPopup(matcher, timeout?): Promise<Page | null>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  matcher: string | RegExp  — match by page title or URL
  timeout: number           — default 10000ms

If matcher is string → checks if title/URL contains it.
If matcher is RegExp → tests against title/URL.
Polls every 500ms until found. Searches from last tab to first.

Returns: Page object or null if not found.

Log: 🔄 Switching to popup: accounts.google.com
Log: ⚠️ Popup not found: ... (timeout: 10000ms)  (when not found)

Example:
```ts
await browser.click('#oauth-btn')
await browser.sleep(2000) // wait for popup to open

const popup = await browser.switchToPopup('accounts.google.com', 15000)
if (popup) {
  await browser.type('#email', 'user@gmail.com')
  await browser.click('#next')
}

// RegExp:
const popup = await browser.switchToPopup(/verify|confirm/i)

// After popup work, switch back:
await browser.switchToDefault()
```

────────────────────────────────────────────────────────────────────────────────
6.11  switchToDefault(): Promise<Page>
────────────────────────────────────────────────────────────────────────────────
Switch back to the default (initial) page stored in context.page.
Typically used after switchToPopup().

Returns: the default Page (always returns, never null).

Log: 🔄 Switching to default page: https://myapp.com

Example:
```ts
const popup = await browser.switchToPopup('oauth')
if (popup) {
  // work in popup...
  await browser.closeCurrentTab()
}
await browser.switchToDefault() // ★ always return to main page
await browser.click('#next-step')
```

────────────────────────────────────────────────────────────────────────────────
6.12  switchToTabIndex(index: number): Promise<Page | null>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  index: number  — 0-based tab index

Returns: Page or null if index is out of range.

Log: 🔄 Switching to tab[0]...
Log: ⚠️ Tab[5] not found (total: 3)

Example:
```ts
await browser.switchToTabIndex(0) // switch to first tab
```

────────────────────────────────────────────────────────────────────────────────
6.13  closeCurrentTab(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Close the current tab. Safe to call even if already closed.

Log: 🗑️ Closing tab: https://example.com/page

Example:
```ts
const popup = await browser.switchToPopup('verify')
if (popup) {
  const code = await browser.getText('#code')
  await browser.closeCurrentTab()
}
await browser.switchToDefault()
```

────────────────────────────────────────────────────────────────────────────────
6.14  closeOtherTabs(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Close all tabs EXCEPT the current one.

Log: 🗑️ Closing 3 other tab(s)...

Example:
```ts
await browser.closeOtherTabs() // keep only current tab
```

────────────────────────────────────────────────────────────────────────────────
6.15  closeAllTabs(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Close ALL tabs including the current one. Full cleanup.

Log: 🗑️ Closing all 4 tab(s)...

Example:
```ts
await browser.closeAllTabs() // close everything
```

────────────────────────────────────────────────────────────────────────────────
6.16  writeOutput(key, value): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  key:   string  — MUST be defined in config.output[]. Throws Error if not.
  value: ProfileOutputValue — see constraints below

Value constraints:
  - string | number | boolean (primitive)
  - Array: max 20 elements, each must be primitive
  - Object: max 10 entries, 1 level deep, values must be primitive

Side effect: updates context.output in real-time.
Side effect: auto-writes to Excel if ExcelStorage is set up.

Log: 📤 Write Output [status] = OK
THROWS: Error if key not in config.output[]
  → Error: [writeOutput] Key "badKey" is not defined in config.output.
    Available keys: [status, title]

Example:
```ts
await browser.writeOutput('status', 'OK')
await browser.writeOutput('title', 'Dashboard')

// Read back immediately:
console.log(context.output.status) // → 'OK'

// Array:
await browser.writeOutput('tags', ['tag1', 'tag2', 'tag3'])

// Object:
await browser.writeOutput('meta', { browser: 'chrome', version: '120' })
```

────────────────────────────────────────────────────────────────────────────────
6.17  writeProfileInput(key, value): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  key:   string  — must be in config.profileInput[]
  value: string | number | boolean (primitive only)

Persists the value back to server/Excel for next run.

Log: ✏️ Write ProfileInput [authToken] = abc123

Example:
```ts
const token = await browser.getText('#token')
if (token) {
  await browser.writeProfileInput('authToken', token)
}
```

────────────────────────────────────────────────────────────────────────────────
6.18  logConfig(config, label?): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Parameters:
  config: Record<string, unknown>  — key-value object to log
  label:  string  — default 'Config'

Example:
```ts
await browser.logConfig({ proxy: '1.2.3.4', country: 'US' }, 'Proxy')
// Log: 📋 Proxy: { proxy: "1.2.3.4", country: "US" }
```

────────────────────────────────────────────────────────────────────────────────
6.19  logProfileInput(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Log all profile input values.
Example: `await browser.logProfileInput()`

────────────────────────────────────────────────────────────────────────────────
6.20  logProfileOutput(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Log all current output values (including previous runs + newly written).
Example: `await browser.logProfileOutput()`

────────────────────────────────────────────────────────────────────────────────
6.21  logGlobalInput(): Promise<void>
────────────────────────────────────────────────────────────────────────────────
Log all global input values.
Example: `await browser.logGlobalInput()`

================================================================================
7. ExcelStorage (Local Dev Only)
================================================================================

IMPORTANT: Call at RUNNER level (main.dev.ts), NOT inside flow class.

```ts
const flow = new MyFlow(AntidetectProvider.GPM)

const storage = flow.createExcelStorage({
  inputFile: {
    filePath: './profiles.xlsx',    // relative to callerDir
    sheetIndex: 0,                  // default: 0
    profileNameColumn: 'A',         // column with profile names
    columns: {
      'B': 'username',              // column B → profileInput.username
      'C': 'password',              // column C → profileInput.password
    },
  },
  outputFile: {
    filePath: './profiles.xlsx',    // same file, different columns
    sheetIndex: 0,
    profileNameColumn: 'A',
    columns: {
      'D': 'status',               // column D ← writeOutput('status', ...)
      'E': 'title',                // column E ← writeOutput('title', ...)
    },
  },
}, __dirname)  // resolve relative paths from this directory

// Read profiles from Excel (row 1 = header, skipped)
const profileSettings = await storage.parseProfileSettings()
// Returns: [{ profileName: 'Profile 1', data: { username: 'admin', password: '123' } }, ...]
```

writeOutput() from BrowserUtils automatically writes to Excel — no manual call needed.

================================================================================
8. DEV RUNNER TEMPLATE (main.dev.ts)
================================================================================

```ts
import { AntidetectProvider } from '@hira-core/sdk'
import MyFlow from './flows/my-flow/v1.0.0'

async function bootstrap() {
  // Pick provider:
  // AntidetectProvider.GPM      — GPM Login (port 19995)
  // AntidetectProvider.HIDEMIUM — Hidemium  (port 2222)
  const flow = new MyFlow(AntidetectProvider.GPM)

  // Excel storage (optional)
  const storage = flow.createExcelStorage({
    inputFile: {
      filePath: './profiles.xlsx',
      profileNameColumn: 'A',
      columns: { 'B': 'username', 'C': 'password' },
    },
    outputFile: {
      filePath: './profiles.xlsx',
      profileNameColumn: 'A',
      columns: { 'D': 'status', 'E': 'title' },
    },
  }, __dirname)

  const profileSettings = await storage.parseProfileSettings()
  console.log(`📊 Loaded ${profileSettings.length} profile(s)`)

  const params = flow.createRunParams({
    antidetect: { profileSettings },
    execution: {
      concurrency: 3,          // profiles running in parallel
      maxRetries: 1,           // retry failed profiles once
      keepOpenOnError: true,   // keep browser open on error
    },
    globalInput: {
      targetUrl: 'https://example.com',
      // ... other global inputs
    },
    window: { width: 1280, height: 720, scale: 0.6 },
  })

  await flow.run(params)
}

bootstrap()
```

Run: `npx ts-node src/main.dev.ts`

================================================================================
9. COMPLETE EXAMPLE FLOW
================================================================================

```ts
import {
  AntidetectBaseFlow, AntidetectProvider, BrowserUtils,
  FlowLogger, IScriptContext, defineFlowConfig,
} from '@hira-core/sdk'

const config = defineFlowConfig({
  globalInput: [
    { key: 'targetUrl', label: 'URL', type: 'text', required: true },
  ],
  profileInput: [
    { key: 'username', label: 'Username', type: 'text', required: true },
    { key: 'password', label: 'Password', type: 'text', required: true },
  ],
  output: [
    { index: 0, key: 'loginStatus', label: 'Status' },
    { index: 1, key: 'pageTitle', label: 'Title' },
  ],
})

type Config = typeof config

export class LoginFlow extends AntidetectBaseFlow<Config> {
  constructor(provider: AntidetectProvider = AntidetectProvider.GPM) {
    super(provider, new FlowLogger('LoginFlow'), config)
  }

  async script(context: IScriptContext<Config>) {
    const browser = new BrowserUtils(context)
    const { globalInput, profileInput, output, logger } = context

    // 1. Log inputs for debugging
    await browser.logProfileInput()
    await browser.logProfileOutput()

    // 2. Skip if already done (from previous run)
    if (output.loginStatus === 'OK') {
      logger.info('Already completed, skipping')
      return
    }

    // 3. Navigate
    await browser.goto(globalInput.targetUrl)

    // 4. Dismiss cookie popup if exists
    const hasCookie = await browser.exists('.cookie-banner', 3000)
    if (hasCookie) await browser.click('.cookie-accept')

    // 5. Login
    await browser.type('#username', profileInput.username)
    await browser.type('#password', profileInput.password)

    const clicked = await browser.click('#login-btn')
    if (!clicked) {
      await browser.writeOutput('loginStatus', 'FAILED - button not found')
      await browser.screenshot('no-button.png')
      return
    }

    // 6. Wait and verify
    await browser.waitForNavigation()
    const loggedIn = await browser.exists('#dashboard', 10000)

    if (loggedIn) {
      const title = await context.page.title()
      await browser.writeOutput('loginStatus', 'OK')
      await browser.writeOutput('pageTitle', title)
      logger.success(`Login OK: ${title}`)
    } else {
      await browser.writeOutput('loginStatus', 'FAILED')
      await browser.screenshot('login-error.png')
    }
  }
}
```

================================================================================
10. POPUP HANDLING PATTERN
================================================================================

```ts
// 1. Trigger popup
await browser.click('#oauth-btn')
await browser.sleep(2000) // wait for popup to open

// 2. Switch to popup
const popup = await browser.switchToPopup('accounts.google.com', 15000)
if (popup) {
  // 3. Work in popup
  await browser.type('#email', profileInput.email)
  await browser.click('#next')
  await browser.sleep(2000)

  // 4. Close popup (optional)
  await browser.closeCurrentTab()
}

// 5. ALWAYS switch back to main page
await browser.switchToDefault()

// 6. Continue on main page
await browser.waitForElement('#success-message')
```

================================================================================
11. PROJECT STRUCTURES
================================================================================

Single flow:
```
my-flow/
├── src/
│   ├── config.ts          ← defineFlowConfig()
│   ├── flow.ts            ← Flow class with script()
│   ├── index.ts           ← export { MyFlow as default }
│   └── main.dev.ts        ← Local dev runner
├── profiles.xlsx
├── package.json
└── tsconfig.json
```

Multi-flow project:
```
my-project/
├── src/
│   ├── flows/
│   │   ├── shared/helpers.ts      ← Reusable utilities
│   │   ├── login-flow/
│   │   │   ├── v1.0.0/index.ts + logic.ts
│   │   │   └── v1.1.0/index.ts + logic.ts  (new version)
│   │   ├── scraper-flow/
│   │   │   └── v1.0.0/index.ts + logic.ts
│   │   └── register-flow/
│   │       └── v1.0.0/index.ts + logic.ts
│   └── main.dev.ts
├── package.json
└── tsconfig.json
```

Version management:
- New version = new folder (v1.0.0/ → v1.1.0/)
- Never modify old versions
- Each version builds independently as a .hira file
- Shared code goes in shared/ folder

================================================================================
12. KEY RULES FOR AI CODE GENERATION
================================================================================

1.  ALWAYS use `defineFlowConfig()` — never raw objects
2.  Flow class MUST extend `AntidetectBaseFlow<typeof config>`
3.  Constructor: `super(provider, new FlowLogger('Name'), config)`
4.  Main logic goes in `async script(context)` — NOT onProfile
5.  Create BrowserUtils: `const browser = new BrowserUtils(context)`
6.  `click()`, `type()`, `goto()`, `waitForNavigation()` return `boolean`
    → ALWAYS check return values!
7.  `writeOutput()` key MUST exist in config.output[] → throws Error if not
8.  `type()` ALWAYS clears existing content first (triple-click + backspace)
9.  `exists()` default timeout is 3000ms (shorter than waitForElement's 10000ms)
10. XPath auto-detected: selectors starting with '//' or '(' → XPath
11. Entry file MUST `export default` the flow class
12. ExcelStorage is set up at runner level (main.dev.ts), NOT inside flow class
13. `switchToDefault()` should ALWAYS be called after switchToPopup() work
14. `writeOutput()` accepts: string | number | boolean | array (max 20) | object (max 10 keys, 1 level)
15. context.output is updated REAL-TIME after writeOutput() — can read back immediately
16. All BrowserUtils methods do NOT throw on failure — check return values instead
17. Only `writeOutput()` throws — if key is invalid
18. Use `context.logger` for custom logging, not console.log
