Skill: liuyu020923/pangolin-ai-serp (version 1.0.2)

--- name: pangolinfo-ai-serp description: > Search Google and get AI Overviews using Pangolin APIs. Use this skill when the user wants to: search Google with AI answers, get search engine results, perform multi-turn AI search conversations, or capture search screenshots. Requires PANGOLIN_EMAIL + PANGOLIN_PASSWORD env vars (or PANGOLIN_API_KEY). --- # Pangolinfo AI SERP Skill Search Google and retrieve AI Overviews, organic search results, and screenshots via Pangolin APIs. ## When to Use This Skill Activate this skill when the user's intent matches any of these patterns: - "Search Google for ...", "Google ...", "Look up ..." - "What does Google say about ...", "Search the web for ..." - "Get AI overview for ...", "AI search for ..." - "Get search results for ...", "SERP for ..." - "Take a screenshot of Google results for ..." - "Follow up on the search with ..." - Chinese equivalents: "搜索一下...", "谷歌搜索...", "帮我查一下...", "用Google搜...", "搜一下...", "AI搜索..." Do **not** use this skill for Amazon product searches, price lookups, or review scraping -- those require a different skill. ## Prerequisites ### Runtime - Python 3.8+ (uses only the standard library -- no `pip install` needed) ### Pangolin Account Register at [pangolinfo.com](https://www.pangolinfo.com) to obtain credentials. ### Environment Variables Set **one** of the following: | Variable | Required | Description | |----------|----------|-------------| | `PANGOLIN_API_KEY` | Option A | API Key (skips login) | | `PANGOLIN_EMAIL` | Option B | Account email | | `PANGOLIN_PASSWORD` | Option B | Account password | API key resolution order: `PANGOLIN_API_KEY` env var > cached `~/.pangolin_api_key` > fresh login. ### macOS SSL Certificate Fix On macOS, Python may fail with `CERTIFICATE_VERIFY_FAILED` because it ships without root certificates by default. **Symptoms:** The script outputs an error with code `SSL_CERT`. **Solutions (pick one):** 1. Run the certificate installer that ships with Python: ```bash /Applications/Python\ 3.x/Install\ Certificates.command ``` (Replace `3.x` with your Python version, e.g. `3.11`) 2. Set the `SSL_CERT_FILE` environment variable: ```bash pip3 install certifi export SSL_CERT_FILE=$(python3 -c "import certifi; print(certifi.where())") ``` ## First-Time Setup Guide When a user tries to use this skill and authentication fails (error code `MISSING_ENV`), **do not just repeat the error hint**. Instead, walk the user through the full setup process interactively: ### Step 1: Explain what's needed Tell the user (in their language): > To use this skill, you need a Pangolin API account. Pangolin provides Google search and AI Overview data through its APIs. > > 使用本技能需要 Pangolin API 账号。Pangolin 提供 Google 搜索和 AI 概览数据的 API 服务。 ### Step 2: Guide registration > 1. Go to [pangolinfo.com](https://www.pangolinfo.com) and create an account > 2. After login, find your API Key in the dashboard > > 1. 访问 [pangolinfo.com](https://www.pangolinfo.com) 注册账号 > 2. 登录后在控制台找到你的 API Key ### Step 3: Collect credentials and authenticate automatically When the user provides their credentials, **you (the AI agent) should configure them securely**. The script will automatically cache the API key at `~/.pangolin_api_key` for all future calls. **If user provides an API key (recommended):** Write it directly to the cache file — avoids shell history entirely: ```bash echo "<api_key>" > ~/.pangolin_api_key chmod 600 ~/.pangolin_api_key 2>/dev/null python3 scripts/pangolin.py --auth-only ``` **If user provides email + password:** Set env vars in the session and clean up after auth: ```bash export PANGOLIN_EMAIL="[REDACTED_SECRET]" export PANGOLIN_PASSWORD="their-password" python3 scripts/pangolin.py --auth-only unset PANGOLIN_EMAIL PANGOLIN_PASSWORD ``` This avoids passwords appearing in shell history (unlike inline `VAR=x command` syntax) and cleans up credentials after the API key is cached. Both methods cache the API key automatically. After this one-time setup, **no environment variables are needed** — all future calls will use the cached API key at `~/.pangolin_api_key`. ### Step 4: Confirm and proceed After auth returns `"success": true`: 1. Tell the user: "认证成功！API Key 已自动缓存，后续使用无需再次输入。" / "Authentication successful! API key cached — no need to enter credentials again." 2. Immediately retry their original request. ### Important - **The user only needs to provide credentials ONCE** — the script caches the API key permanently at `~/.pangolin_api_key` - Do not ask the user to manually edit `.bashrc` or `.zshrc` — the script handles persistence automatically - If the user doesn't have an account yet, explain Pangolin's credit system (2 credits per AI Mode search, 0.5 credits per SERP search) and direct them to [pangolinfo.com](https://www.pangolinfo.com) - If auth succeeds but credits are exhausted (error code `2001`), tell the user to top up at pangolinfo.com - API key is permanent and does not expire unless the account is deactivated ## Script Execution The main script is located at `scripts/pangolin.py` relative to this skill directory. **Path resolution:** When invoking the script, resolve the absolute path from this skill's directory. Example: ```bash SKILL_DIR="$(cd "$(dirname "$0")" && pwd)" python3 "$SKILL_DIR/scripts/pangolin.py" --q "your query" ``` Or invoke directly: ```bash python3 scripts/pangolin.py --q "your query" ``` ## Intent-to-Command Mapping ### AI Mode Search (default) User wants AI-generated answers with references. This is the default mode. ```bash python3 scripts/pangolin.py --q "what is quantum computing" ``` ### Standard SERP User wants traditional Google search results (organic links + optional AI overview). ```bash python3 scripts/pangolin.py --q "best databases 2025" --mode serp ``` ### SERP with Screenshot User wants to see the actual Google results page. ```bash python3 scripts/pangolin.py --q "best databases 2025" --mode serp --screenshot ``` ### SERP with Region User wants SERP results from a specific geographic region. ```bash python3 scripts/pangolin.py --q "best databases 2025" --mode serp --region us ``` ### Multi-Turn Dialogue User wants to ask follow-up questions in an AI Mode conversation. ```bash python3 scripts/pangolin.py --q "kubernetes" --follow-up "how to deploy" --follow-up "monitoring tools" ``` ### Auth Check Verify credentials are working without consuming credits. ```bash python3 scripts/pangolin.py --auth-only ``` ## Smart Defaults | Parameter | Default | Notes | |-----------|---------|-------| | `--mode` | `ai-mode` | AI Mode with Google AI Overviews | | `--num` | `10` | Number of results to request | | `--screenshot` | off | Pass flag to enable | | `--follow-up` | none | Repeatable; keep to 5 or fewer for speed | When the user simply says "search for X" without specifying a mode, use the default `ai-mode`. ## Output Format The script outputs JSON to **stdout** on success and structured error JSON to **stderr** on failure. ### Success Example (AI Mode) ```json { "success": true, "task_id": "1768988520324-766a695d93b57aad", "results_num": 1, "ai_overview_count": 1, "ai_overview": [ { "content": ["Quantum computing uses quantum bits (qubits)..."], "references": [ { "title": "Quantum Computing - Wikipedia", "url": "https://en.wikipedia.org/wiki/Quantum_computing", "domain": "Wikipedia" } ] } ], "screenshot": "https://image.datasea.network/screenshots/..." } ``` ### Success Example (SERP) ```json { "success": true, "task_id": "1768988520324-abcdef123456", "results_num": 3, "ai_overview_count": 1, "ai_overview": [ { "content": ["Java works by compiling source code..."], "references": [ {"title": "How Java Works", "url": "https://docs.oracle.com/...", "domain": "Oracle"} ] } ], "organic_results": [ { "title": "Java Tutorial for Beginners", "url": "https://example.com/java-tutorial", "text": "Learn how Java works from the ground up..." } ] } ``` ### Error Example ```json { "success": false, "error": { "code": "MISSING_ENV", "message": "No authentication credentials found.", "hint": "Set PANGOLIN_API_KEY, or both PANGOLIN_EMAIL and PANGOLIN_PASSWORD environment variables." } } ``` ## Response Presentation When presenting results to the user: 1. **Use natural language** -- never dump raw JSON to the user. 2. **Match the user's language** -- if the user writes in Chinese, respond in Chinese; if English, respond in English. 3. **Summarize the AI overview** first (if present), then list relevant organic results with titles and URLs. 4. **Include source URLs** so the user can verify or explore further. 5. **Mention the screenshot URL** if one was captured, so the user can view the rendered page. 6. **If no AI overview** is present, note that Google did not generate one for this query and present organic results instead. 7. **On error**, explain the issue in plain language and suggest corrective action based on the `hint` field. ## All CLI Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `--q` | string | *required* | Search query | | `--mode` | `ai-mode` \| `serp` | `ai-mode` | API mode | | `--screenshot` | flag | off | Capture page screenshot | | `--follow-up` | string (repeatable) | none | Follow-up question (ai-mode only) | | `--num` | int | `10` | Number of results | | `--region` | string | none | Geographic region for SERP results (e.g., `us`, `uk`). SERP mode only. | | `--auth-only` | flag | off | Auth check only (no query needed) | | `--raw` | flag | off | Output raw API response | ## Output Schema See [references/output-schema.md](references/output-schema.md) for the complete JSON output schema documentation. ## Cost - **AI Mode:** 2 credits per request - **SERP:** 0.5 credits per request Credits are only consumed on successful requests (API code 0). Auth checks (`--auth-only`) do not consume credits. ## Exit Codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | API error (non-zero code from Pangolin) | | 2 | Usage error (bad arguments) | | 3 | Network error (connection, SSL, timeout, rate limit) | | 4 | Authentication error | ## Troubleshooting ### Error Communication Table | Error Code | Meaning | User-Facing Message | Resolution | |------------|---------|---------------------|------------| | `MISSING_ENV` | No credentials | "Authentication credentials are not configured." | Set `PANGOLIN_EMAIL` + `PANGOLIN_PASSWORD` or `PANGOLIN_API_KEY`. | | `AUTH_FAILED` | Wrong credentials | "Authentication failed. Please check your credentials." | Verify email and password are correct. | | `RATE_LIMIT` | Too many requests | "The API is rate-limiting requests. Please wait and try again." | Wait a moment, then retry. | | `NETWORK` | Connection issue | "A network error occurred. Please check your connection." | Check internet, firewall, proxy settings. | | `SSL_CERT` | Certificate error | "SSL certificate verification failed." | See macOS SSL Certificate Fix section above. | | `API_ERROR` | Pangolin API error | "The search API returned an error." | Check the `hint` field; see `references/error-codes.md`. | ### Pangolin API Error Codes | API Code | Meaning | Resolution | |----------|---------|------------| | 1004 | Invalid/expired API key | Auto-retried by the script. If persistent, delete `~/.pangolin_api_key` and retry. | | 2001 | Insufficient credits | Top up credits at pangolinfo.com. | | 2007 | Account expired | Renew subscription at pangolinfo.com. | | 10000 | Task execution failed | Retry the request. Check query format. | | 10001 | Task execution failed | Retry. May be a temporary server issue. | ## Important Notes for AI Agents 1. **Always run `--auth-only` first** if you are unsure whether credentials are configured, before spending credits on a real query. 2. **Default to `ai-mode`** unless the user explicitly asks for standard/traditional search results. 3. **Never expose raw JSON** to the user. Parse the output and present it in natural language. 4. **Respect the user's language.** Respond in the same language the user is writing in. 5. **Keep follow-ups to 5 or fewer** for optimal response time. Warn the user if they request more. 6. **Handle errors gracefully.** If the script exits with a non-zero code, read stderr for the structured error and present the `hint` to the user. 7. **Do not log or echo API keys, passwords, or cookies** in any output, logs, or error messages. 8. **Credit awareness:** AI Mode costs 2 credits per search; SERP costs 0.5 credits. If a user requests many searches in a row, mention the credit cost. 9. **Screenshot is optional.** Only pass `--screenshot` when the user explicitly wants to see the rendered page or when visual context is needed. 10. **Multi-turn is ai-mode only.** Do not attempt `--follow-up` with `--mode serp`.

path: references/ai-mode-api.md
content: # AI Mode API Reference

Google AI Mode search via `udm=50` parameter. Returns AI-generated answers with references.

## Endpoint

```
POST https://scrapeapi.pangolinfo.com/api/v2/scrape
```

**Headers:**
- `Content-Type: application/json`
- `Authorization: Bearer <token>`

## Request Body

| Field | Required | Type | Description |
|-------|----------|------|-------------|
| url | Yes | string | Google search URL with `udm=50` |
| parserName | Yes | string | Must be `"googleAISearch"` |
| screenshot | No | boolean | Capture page screenshot |
| param | No | string[] | Follow-up questions for multi-turn dialogue |

### URL Format

```
https://www.google.com/search?num=10&udm=50&q=<encoded_query>
```

- `udm=50` enables Google AI Mode results
- `num` controls the number of results
- `q` is the URL-encoded search query

### Single Query Example

```json
{
  "url": "https://www.google.com/search?num=10&udm=50&q=javascript",
  "parserName": "googleAISearch",
  "screenshot": true
}
```

### Multi-Turn Example

```json
{
  "url": "https://www.google.com/search?num=10&udm=50&q=javascript",
  "parserName": "googleAISearch",
  "param": ["how to setup", "best frameworks"]
}
```

The `param` array contains follow-up questions. Each element is processed as a subsequent turn in the conversation. When the array length exceeds 5, response efficiency decreases -- keep it at 5 or fewer for optimal performance.

## Response

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "results_num": 1,
    "ai_overview": 1,
    "json": {
      "type": "organic",
      "items": [
        {
          "type": "ai_overview",
          "items": [
            {
              "type": "ai_overview_elem",
              "content": [
                "JavaScript is a high-level programming language..."
              ]
            }
          ],
          "references": [
            {
              "type": "ai_overview_reference",
              "title": "JavaScript - Wikipedia",
              "url": "https://en.wikipedia.org/wiki/JavaScript",
              "domain": "Wikipedia"
            }
          ]
        }
      ]
    },
    "screenshot": "https://image.datasea.network/screenshots/...",
    "taskId": "1768988520324-766a695d93b57aad",
    "url": "https://www.google.com/search?num=10&udm=50&q=javascript"
  }
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| code | int | `0` = success, see error-codes.md for others |
| data.results_num | int | Number of result items |
| data.ai_overview | int | Number of AI overview blocks |
| data.json.items[] | array | Result items (ai_overview, organic) |
| data.screenshot | string | Screenshot URL (if requested) |
| data.taskId | string | Unique task identifier |

### AI Overview Item Structure

| Field | Type | Description |
|-------|------|-------------|
| type | string | `"ai_overview"` |
| items[].type | string | `"ai_overview_elem"` |
| items[].content | string[] | AI-generated text paragraphs |
| references[].type | string | `"ai_overview_reference"` |
| references[].title | string | Source page title |
| references[].url | string | Source page URL |
| references[].domain | string | Source domain name |

## Cost

**2 credits per request** for complete AI Overview information retrieval.


path: references/ai-overview-serp-api.md
content: # AI Overview SERP API Reference

Standard Google search results with AI Overview extraction. Returns both organic results and AI-generated overview when available.

## Endpoint

```
POST https://scrapeapi.pangolinfo.com/api/v2/scrape
```

**Headers:**
- `Content-Type: application/json`
- `Authorization: Bearer <token>`

## Request Body

| Field | Required | Type | Description |
|-------|----------|------|-------------|
| url | Yes | string | Standard Google search URL (no `udm=50`) |
| parserName | Yes | string | Must be `"googleSearch"` |
| screenshot | No | boolean | Capture page screenshot |

### URL Format

```
https://www.google.com/search?num=10&q=<encoded_query>
```

No `udm=50` parameter -- this uses standard Google search.

### Request Example

```json
{
  "url": "https://www.google.com/search?num=10&q=how+java+work",
  "parserName": "googleSearch",
  "screenshot": true
}
```

## Response

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "results_num": 3,
    "ai_overview": 1,
    "json": {
      "type": "organic",
      "items": [
        {
          "type": "ai_overview",
          "items": [
            {
              "type": "ai_overview_elem",
              "content": ["Java works by compiling source code..."]
            }
          ],
          "references": [
            {
              "type": "ai_overview_reference",
              "title": "How Java Works - Oracle",
              "url": "https://docs.oracle.com/...",
              "domain": "Oracle"
            }
          ]
        },
        {
          "type": "organic",
          "items": [
            {
              "type": "organic",
              "url": "https://example.com/java-tutorial",
              "title": "Java Tutorial for Beginners",
              "text": "Learn how Java works from the ground up..."
            }
          ]
        }
      ]
    },
    "screenshot": "https://image.datasea.network/screenshots/...",
    "taskId": "1768988520324-abcdef123456"
  }
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| code | int | `0` = success, see error-codes.md for others |
| data.results_num | int | Total number of result items |
| data.ai_overview | int | `1` if AI overview present, `0` if not |
| data.json.items[] | array | Mixed array of ai_overview and organic items |
| data.screenshot | string | Screenshot URL (if requested) |
| data.taskId | string | Unique task identifier |

### AI Overview Detection

- Check `data.ai_overview == 1` to confirm presence
- The AI overview item appears first in the `items` array with `type: "ai_overview"`
- Not all queries trigger an AI overview -- the field will be `0` when absent

### Organic Result Structure

| Field | Type | Description |
|-------|------|-------------|
| type | string | `"organic"` |
| items[].url | string | Result page URL |
| items[].title | string | Result page title |
| items[].text | string | Result snippet text |

## Differences from AI Mode API

| Aspect | AI Overview SERP | AI Mode |
|--------|-----------------|---------|
| parserName | `googleSearch` | `googleAISearch` |
| URL param | no `udm=50` | requires `udm=50` |
| Multi-turn | Not supported | Supported via `param` |
| Results | Organic + optional AI overview | Primarily AI overview |

## Cost

**2 credits per request** for complete AI Overview information retrieval.


path: references/error-codes.md
content: # Error Codes and Troubleshooting

## Error Code Reference

| Code | Meaning | Resolution |
|------|---------|------------|
| 0 | Success | No action needed |
| 1004 | Invalid or expired token | Re-authenticate with email/password. The script does this automatically. |
| 2001 | Insufficient credits | Top up credits at pangolinfo.com |
| 2007 | Account expired | Renew account subscription at pangolinfo.com |
| 10000 | Task execution failed | Retry the request. If persistent, check query/URL format. |
| 10001 | Task execution failed | Retry the request. May indicate a temporary server issue. |
| 404 | Incorrect URL address | Verify the target URL format |

## Authentication

### Token Lifecycle

- Tokens are **permanent** -- they do not expire on their own
- A token becomes invalid only if the account is deactivated
- Error code `1004` indicates the token needs to be refreshed
- The script caches tokens at `~/.pangolin_token`

### Token Resolution Order

1. `PANGOLIN_TOKEN` environment variable
2. Cached token at `~/.pangolin_token`
3. Fresh login using `PANGOLIN_EMAIL` + `PANGOLIN_PASSWORD`

### Auth Endpoint

```
POST https://scrapeapi.pangolinfo.com/api/v1/auth
Body: {"email": "<email>", "password": "<password>"}
Response: {"code": 0, "message": "ok", "data": "<token>"}
```

## Credit Management

- AI Mode API: **2 credits** per request
- AI Overview SERP API: **2 credits** per request
- Amazon Scrape API (json): **1 credit** per request
- Amazon Scrape API (rawHtml/markdown): **0.75 credits** per request
- Credits are only consumed on successful requests (code 0)
- Check your credit balance at [pangolinfo.com](https://www.pangolinfo.com)

## Common Issues

**"No authentication credentials" error**
Set environment variables: `export PANGOLIN_EMAIL=... PANGOLIN_PASSWORD=...`

**Empty AI overview in response**
Not all queries trigger an AI overview. Check `ai_overview` field in response. Try a more informational query.

**Timeout or network errors**
The script retries 3 times with exponential backoff. If all retries fail, check your network connection and the API status.

**Screenshot URL not returned**
Ensure `--screenshot` flag is passed (or `"screenshot": true` in request body).


path: references/examples/ai-mode-example.json
content: {
  "success": true,
  "task_id": "1768988520324-766a695d93b57aad",
  "results_num": 1,
  "ai_overview_count": 1,
  "ai_overview": [
    {
      "content": [
        "Quantum computing is a type of computation that uses quantum-mechanical phenomena such as superposition and entanglement to process information.",
        "Unlike classical computers that use bits (0 or 1), quantum computers use quantum bits, or qubits, which can exist in multiple states simultaneously.",
        "Key concepts include: superposition (a qubit can be in a combination of 0 and 1 states), entanglement (qubits can be correlated with each other), and quantum gates (operations that manipulate qubits).",
        "Potential applications include cryptography, drug discovery, optimization problems, and machine learning acceleration."
      ],
      "references": [
        {
          "title": "Quantum Computing - Wikipedia",
          "url": "https://en.wikipedia.org/wiki/Quantum_computing",
          "domain": "Wikipedia"
        },
        {
          "title": "What Is Quantum Computing? - IBM",
          "url": "https://www.ibm.com/topics/quantum-computing",
          "domain": "IBM"
        },
        {
          "title": "Quantum Computing Explained - MIT Technology Review",
          "url": "https://www.technologyreview.com/2024/01/quantum-computing-explained/",
          "domain": "MIT Technology Review"
        }
      ]
    }
  ],
  "screenshot": "https://image.datasea.network/screenshots/example-quantum-computing-ai-mode.png"
}


path: references/examples/serp-example.json
content: {
  "success": true,
  "task_id": "1768988520324-abcdef123456",
  "results_num": 5,
  "ai_overview_count": 1,
  "ai_overview": [
    {
      "content": [
        "The best databases in 2025 depend on your use case. For relational databases, PostgreSQL and MySQL remain top choices. For NoSQL, MongoDB and Redis are widely adopted.",
        "Cloud-native options like Amazon Aurora, Google Cloud Spanner, and CockroachDB are gaining traction for distributed workloads."
      ],
      "references": [
        {
          "title": "Best Databases 2025 - DB-Engines Ranking",
          "url": "https://db-engines.com/en/ranking",
          "domain": "DB-Engines"
        },
        {
          "title": "Top Databases Compared - TechRadar",
          "url": "https://www.techradar.com/best/best-databases",
          "domain": "TechRadar"
        }
      ]
    }
  ],
  "organic_results": [
    {
      "title": "Best Databases in 2025: A Comprehensive Guide",
      "url": "https://www.example.com/best-databases-2025",
      "text": "We compared 15 popular databases across performance, scalability, and ease of use to find the best options for 2025..."
    },
    {
      "title": "DB-Engines Ranking - Popularity Ranking of Database Management Systems",
      "url": "https://db-engines.com/en/ranking",
      "text": "The DB-Engines Ranking ranks database management systems according to their popularity. The ranking is updated monthly..."
    },
    {
      "title": "PostgreSQL vs MySQL vs MongoDB: Which Database to Choose in 2025",
      "url": "https://www.example.com/postgres-vs-mysql-vs-mongo",
      "text": "A detailed comparison of the three most popular open-source databases, covering performance benchmarks and real-world use cases..."
    },
    {
      "title": "Top 10 Cloud Databases for Modern Applications",
      "url": "https://www.example.com/cloud-databases-2025",
      "text": "Cloud-native databases offer automatic scaling, high availability, and managed operations. Here are the top 10 picks for 2025..."
    },
    {
      "title": "Choosing the Right Database: SQL vs NoSQL in 2025",
      "url": "https://www.example.com/sql-vs-nosql-2025",
      "text": "Understanding when to use SQL versus NoSQL databases is crucial for application architecture. This guide covers the key decision factors..."
    }
  ],
  "screenshot": "https://image.datasea.network/screenshots/example-best-databases-serp.png"
}


path: references/output-schema.md
content: # Output Schema Reference

This document describes the JSON output schema produced by `scripts/pangolin.py`.

## Envelope Structure

All output follows one of two envelope formats: **success** or **error**.

### Success Envelope

Printed to **stdout** when the API returns a successful response (code 0).

```json
{
  "success": true,
  "task_id": "<string>",
  "results_num": "<int>",
  "ai_overview_count": "<int>",
  "ai_overview": [ ... ],
  "organic_results": [ ... ],
  "screenshot": "<string>"
}
```

### Error Envelope

Printed to **stderr** when any error occurs (network, auth, API, etc.).

```json
{
  "success": false,
  "error": {
    "code": "<string>",
    "message": "<string>",
    "hint": "<string>"
  }
}
```

## Success Fields

| Field | Type | Guaranteed | Description |
|-------|------|------------|-------------|
| `success` | boolean | Yes | Always `true` for successful responses |
| `task_id` | string | Yes | Unique Pangolin task identifier |
| `results_num` | int | Yes | Total number of result items returned |
| `ai_overview_count` | int | Yes | Number of AI overview blocks (0 if none) |
| `ai_overview` | array | No | Present only if AI overviews exist |
| `organic_results` | array | No | Present only if organic results exist |
| `screenshot` | string | No | Screenshot URL; present only if `--screenshot` was passed |

### `ai_overview[]` Item

Each AI overview block contains the AI-generated content and its source references.

| Field | Type | Guaranteed | Description |
|-------|------|------------|-------------|
| `content` | string[] | Yes | Array of AI-generated text paragraphs |
| `references` | array | Yes | Array of source references (may be empty) |

### `ai_overview[].references[]` Item

| Field | Type | Guaranteed | Description |
|-------|------|------------|-------------|
| `title` | string | Yes | Source page title |
| `url` | string | Yes | Source page URL |
| `domain` | string | Yes | Source domain name |

### `organic_results[]` Item

Each organic result represents a traditional Google search result link.

| Field | Type | Guaranteed | Description |
|-------|------|------------|-------------|
| `title` | string | Yes | Result page title |
| `url` | string | Yes | Result page URL |
| `text` | string | No | Result snippet text (may be null) |

## Error Fields

| Field | Type | Guaranteed | Description |
|-------|------|------------|-------------|
| `success` | boolean | Yes | Always `false` for error responses |
| `error.code` | string | Yes | Machine-readable error code |
| `error.message` | string | Yes | Human-readable error description |
| `error.hint` | string | No | Suggested resolution (present for most errors) |

### Error Codes

| Code | Trigger |
|------|---------|
| `MISSING_ENV` | No credentials found in environment or cache |
| `AUTH_FAILED` | Email/password authentication rejected |
| `RATE_LIMIT` | HTTP 429 from the API |
| `NETWORK` | Connection timeout, DNS failure, or other network error |
| `SSL_CERT` | SSL certificate verification failed (common on macOS) |
| `API_ERROR` | Non-zero response code from Pangolin API |

## Mode Differences

### AI Mode (`--mode ai-mode`)

- Typically returns `ai_overview` with rich AI-generated content
- `organic_results` may or may not be present
- Supports `--follow-up` for multi-turn dialogue

### SERP Mode (`--mode serp`)

- Returns `organic_results` with traditional search results
- `ai_overview` may be present if Google generated one for the query
- Does **not** support `--follow-up`

## Auth-Only Output

When using `--auth-only`, the output is a simplified success object:

```json
{
  "success": true,
  "message": "Authentication successful",
  "token_preview": "eyJhbGci...ab1c"
}
```

The `token_preview` shows only the first 8 and last 4 characters -- the full token is never printed.

## Raw Mode

When using `--raw`, the output is the unprocessed Pangolin API response. The structure matches the Pangolin API documentation in `references/ai-mode-api.md` and `references/ai-overview-serp-api.md`. The envelope structure described above does **not** apply in raw mode.

## Example Files

See `references/examples/` for complete example outputs:

- `ai-mode-example.json` -- AI Mode search result
- `serp-example.json` -- Standard SERP search result


path: scripts/pangolin.py
content: #!/usr/bin/env python3
"""
Pangolin Google SERP & AI Mode API Client

Zero-dependency Python client for Pangolin's Google SERP and AI Mode APIs.
Supports AI Mode search (Google AI Overviews), standard SERP results,
multi-turn dialogue, and screenshot capture.

Usage:
    pangolin.py --q "quantum computing"
    pangolin.py --q "best databases 2025" --mode serp --screenshot
    pangolin.py --q "kubernetes" --follow-up "how to deploy"
    pangolin.py --auth-only

Environment:
    PANGOLIN_API_KEY  - API Key (skips login)
    PANGOLIN_EMAIL    - Account email (for login)
    PANGOLIN_PASSWORD - Account password (for login)
"""

import argparse
import io
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

# ---------------------------------------------------------------------------
# Fix Windows / macOS console encoding for Unicode output
# ---------------------------------------------------------------------------
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
    sys.stdout = io.TextIOWrapper(
        sys.stdout.buffer, encoding="utf-8", errors="replace"
    )
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
    sys.stderr = io.TextIOWrapper(
        sys.stderr.buffer, encoding="utf-8", errors="replace"
    )

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
API_BASE = "https://scrapeapi.pangolinfo.com"
AUTH_ENDPOINT = f"{API_BASE}/api/v1/auth"
SCRAPE_V1_ENDPOINT = f"{API_BASE}/api/v1/scrape"
SCRAPE_V2_ENDPOINT = f"{API_BASE}/api/v2/scrape"
API_KEY_CACHE_PATH = Path.home() / ".pangolin_api_key"

EXIT_SUCCESS = 0
EXIT_API_ERROR = 1
EXIT_USAGE_ERROR = 2
EXIT_NETWORK_ERROR = 3
EXIT_AUTH_ERROR = 4


# ---------------------------------------------------------------------------
# Unified error helper
# ---------------------------------------------------------------------------
def _emit_error(code, message, hint=None, exit_code=None):
    """Print a structured error envelope to stderr and optionally exit.

    NEVER include API keys, passwords, or cookies in error output.
    """
    envelope = {
        "success": False,
        "error": {
            "code": code,
            "message": message,
        },
    }
    if hint:
        envelope["error"]["hint"] = hint
    print(json.dumps(envelope), file=sys.stderr)
    if exit_code is not None:
        sys.exit(exit_code)


# ---------------------------------------------------------------------------
# API key caching
# ---------------------------------------------------------------------------
def load_cached_api_key():
    """Load API key from cache file if it exists."""
    if API_KEY_CACHE_PATH.exists():
        [REDACTED_SECRET]).strip()
        if api_key and len(api_key.split(".")) == 3:  # Basic JWT format check
            return api_key
    return None


def save_cached_api_key(api_key):
    """Save API key to cache file."""
    API_KEY_CACHE_PATH.write_text(api_key)
    try:
        API_KEY_CACHE_PATH.chmod(0o600)
    except OSError:
        pass
    # Windows: restrict file access to current user
    if sys.platform == "win32":
        try:
            import subprocess
            subprocess.run(
                ["icacls", str(API_KEY_CACHE_PATH), "/inheritance:r",
                 "/grant:r", f"{os.environ.get('USERNAME', '')}:F"],
                capture_output=True, check=False,
            )
        except Exception:
            pass


# ---------------------------------------------------------------------------
# Authentication
# ---------------------------------------------------------------------------
def authenticate(email, password):
    """Authenticate with Pangolin API and return an API key."""
    body = json.dumps({"email": email, "password": password}).encode()
    req = urllib.request.Request(
        AUTH_ENDPOINT,
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read())
    except urllib.error.URLError as e:
        # Improvement 1: macOS SSL certificate handling
        if "CERTIFICATE_VERIFY_FAILED" in str(e) or "SSL" in str(e):
            _emit_error(
                "SSL_CERT",
                "SSL certificate verification failed",
                hint=(
                    "macOS users: run '/Applications/Python 3.x/Install Certificates.command' "
                    "or set SSL_CERT_FILE env var. "
                    "See: python3 -c \"import certifi; print(certifi.where())\""
                ),
                exit_code=EXIT_NETWORK_ERROR,
            )
        _emit_error(
            "NETWORK",
            "Network error during authentication",
            hint="Check your internet connection and try again.",
            exit_code=EXIT_NETWORK_ERROR,
        )

    if result.get("code") != 0:
        _emit_error(
            "AUTH_FAILED",
            "Authentication failed",
            hint="Verify PANGOLIN_EMAIL and PANGOLIN_PASSWORD are correct.",
            exit_code=EXIT_AUTH_ERROR,
        )

    [REDACTED_SECRET]"data"]
    save_cached_api_key(api_key)
    return api_key


def get_api_key():
    """Resolve API key from env var, cache file, or fresh login."""
    [REDACTED_SECRET]"PANGOLIN_API_KEY")
    if [REDACTED_SECRET])  # Cache for future calls without env var
        return api_key

    [REDACTED_SECRET])
    if [REDACTED_SECRET] api_key

    email = os.environ.get("PANGOLIN_EMAIL")
    [REDACTED_SECRET]"PANGOLIN_PASSWORD")
    if not email or not [REDACTED_SECRET]
            "MISSING_ENV",
            "No authentication credentials found.",
            hint=(
                "Set PANGOLIN_API_KEY, or both PANGOLIN_EMAIL and PANGOLIN_PASSWORD "
                "environment variables."
            ),
            exit_code=EXIT_AUTH_ERROR,
        )

    return authenticate(email, password)


def refresh_api_key():
    """Force re-authentication using email/password."""
    email = os.environ.get("PANGOLIN_EMAIL")
    [REDACTED_SECRET]"PANGOLIN_PASSWORD")
    if not email or not [REDACTED_SECRET]
            "MISSING_ENV",
            "Cannot refresh API key without credentials.",
            hint="Set PANGOLIN_EMAIL and PANGOLIN_PASSWORD environment variables.",
            exit_code=EXIT_AUTH_ERROR,
        )
    return authenticate(email, password)



# ---------------------------------------------------------------------------
# Request building (Google only)
# ---------------------------------------------------------------------------
def build_google_body(query, mode, screenshot, follow_ups, num, region=None):
    """Build request body for Google SERP / AI Mode APIs."""
    if mode == "ai-mode":
        url = (
            f"https://www.google.com/search?num={num}&udm=50"
            f"&q={urllib.parse.quote_plus(query)}"
        )
        body = {
            "url": url,
            "parserName": "googleAISearch",
        }
    else:
        url = (
            f"https://www.google.com/search?num={num}"
            f"&q={urllib.parse.quote_plus(query)}"
        )
        body = {
            "url": url,
            "parserName": "googleSearch",
            "format": "json",
            "scrapeContext": {
                "resultNum": num,
            },
        }
        if region:
            body["scrapeContext"]["region"] = region

    if screenshot:
        body["screenshot"] = True

    if follow_ups:
        body["param"] = follow_ups

    return body


# ---------------------------------------------------------------------------
# API call with retry
# ---------------------------------------------------------------------------
def call_api(api_key, body, endpoint, max_retries=3, timeout=120):
    """Call the scrape API with retry and exponential backoff."""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
        "User-Agent": "Pangolin-CLI/1.0",
    }
    payload = json.dumps(body).encode()

    for attempt in range(max_retries):
        try:
            req = urllib.request.Request(
                endpoint,
                data=payload,
                headers=headers,
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as e:
            error_body = e.read().decode() if e.fp else ""
            if e.code == 429:
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
                _emit_error(
                    "RATE_LIMIT",
                    "Rate limited by API server.",
                    hint="Wait a moment and retry, or reduce request frequency.",
                    exit_code=EXIT_NETWORK_ERROR,
                )
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            _emit_error(
                "API_ERROR",
                f"HTTP {e.code} from API.",
                hint="Check your request parameters and try again.",
                exit_code=EXIT_NETWORK_ERROR,
            )
        except urllib.error.URLError as e:
            # Improvement 1: macOS SSL certificate handling
            if "CERTIFICATE_VERIFY_FAILED" in str(e) or "SSL" in str(e):
                _emit_error(
                    "SSL_CERT",
                    "SSL certificate verification failed",
                    hint=(
                        "macOS users: run '/Applications/Python 3.x/Install Certificates.command' "
                        "or set SSL_CERT_FILE env var. "
                        "See: python3 -c \"import certifi; print(certifi.where())\""
                    ),
                    exit_code=EXIT_NETWORK_ERROR,
                )
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            _emit_error(
                "NETWORK",
                "Network error communicating with API.",
                hint="Check your internet connection and try again.",
                exit_code=EXIT_NETWORK_ERROR,
            )

    return None


# ---------------------------------------------------------------------------
# Response handling
# ---------------------------------------------------------------------------
def handle_response(result, api_key, body, endpoint, timeout=120):
    """Handle API response, retrying auth on 1004 error."""
    if result.get("code") == 1004:
        new_api_key = refresh_api_key()
        return call_api(new_api_key, body, endpoint, timeout=timeout)
    return result


def extract_google_output(result):
    """Extract structured output from Google SERP / AI Mode API response."""
    code = result.get("code")
    if code != 0:
        return {
            "success": False,
            "error": {
                "code": "API_ERROR",
                "message": result.get("message", "Unknown API error"),
                "hint": f"Pangolin API returned error code {code}. See references/error-codes.md.",
            },
        }

    data = result.get("data", {})
    output = {
        "success": True,
        "task_id": data.get("taskId"),
        "results_num": data.get("results_num", 0),
        "ai_overview_count": data.get("ai_overview", 0),
    }

    json_data = data.get("json", {})

    # v1 (SERP) returns json as array: [{code, data: {items: [...]}}]
    # v2 (AI Mode) returns json as object: {items: [...]}
    if isinstance(json_data, list) and len(json_data) > 0:
        inner = json_data[0]
        items = inner.get("data", {}).get("items", [])
    elif isinstance(json_data, dict):
        items = json_data.get("items", [])
    else:
        items = []

    ai_overviews = []
    organic_results = []

    for item in items:
        item_type = item.get("type")
        if item_type == "ai_overview":
            overview = {"content": [], "references": []}
            for sub in item.get("items", []):
                if sub.get("type") == "ai_overview_elem":
                    overview["content"].extend(sub.get("content", []))
            for ref in item.get("references", []):
                overview["references"].append({
                    "title": ref.get("title"),
                    "url": ref.get("url"),
                    "domain": ref.get("domain"),
                })
            ai_overviews.append(overview)
        elif item_type == "organic":
            for sub in item.get("items", []):
                organic_results.append({
                    "title": sub.get("title"),
                    "url": sub.get("url"),
                    "text": sub.get("text"),
                })

    if ai_overviews:
        output["ai_overview"] = ai_overviews
        output["ai_overview_count"] = len(ai_overviews)
    if organic_results:
        output["organic_results"] = organic_results
        output["results_num"] = len(organic_results)

    screenshot_url = data.get("screenshot")
    if screenshot_url:
        output["screenshot"] = screenshot_url

    return output


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(
        description="Pangolin Google SERP & AI Mode API Client",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  # AI Mode search (default)\n"
            '  python3 scripts/pangolin.py --q "what is quantum computing"\n'
            "\n"
            "  # Standard SERP with screenshot\n"
            '  python3 scripts/pangolin.py --q "best databases 2025" --mode serp --screenshot\n'
            "\n"
            "  # Multi-turn follow-up\n"
            '  python3 scripts/pangolin.py --q "kubernetes" --follow-up "how to deploy" '
            '--follow-up "monitoring tools"\n'
            "\n"
            "  # Auth check\n"
            "  python3 scripts/pangolin.py --auth-only\n"
            "\n"
            "Environment variables:\n"
            "  PANGOLIN_API_KEY    API Key (skips login)\n"
            "  PANGOLIN_EMAIL      Account email\n"
            "  PANGOLIN_PASSWORD   Account password\n"
        ),
    )
    parser.add_argument("--q", dest="query", help="Search query")
    parser.add_argument(
        "--mode",
        choices=["ai-mode", "serp"],
        default="ai-mode",
        help="API mode: ai-mode (default) | serp",
    )
    parser.add_argument(
        "--screenshot", action="store_true", help="Capture page screenshot"
    )
    parser.add_argument(
        "--follow-up",
        action="append",
        dest="follow_ups",
        help=(
            "Follow-up question for multi-turn (ai-mode only). "
            "Can be repeated. >5 may slow response."
        ),
    )
    parser.add_argument(
        "--num",
        type=int,
        default=10,
        help="Number of results (default: 10)",
    )
    parser.add_argument(
        "--region",
        default=None,
        help="Geographic region for SERP results (e.g., us, uk). SERP mode only.",
    )
    parser.add_argument(
        "--auth-only",
        action="store_true",
        help="Only authenticate and print API key info",
    )
    parser.add_argument(
        "--raw",
        action="store_true",
        help="Output raw API response instead of extracted data",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=120,
        help="API request timeout in seconds (default: 120)",
    )

    args = parser.parse_args()

    # Validate inputs
    if not args.query and not args.auth_only:
        parser.error("--q is required unless using --auth-only")

    if args.follow_ups and args.mode != "ai-mode":
        parser.error("--follow-up is only supported in ai-mode")

    if args.follow_ups and len(args.follow_ups) > 5:
        print(json.dumps({"warning": f"Using {len(args.follow_ups)} follow-ups (>5). Response may be slower."}), file=sys.stderr)

    # Authenticate
    [REDACTED_SECRET])

    if args.auth_only:
        print(json.dumps({
            "success": True,
            "message": "Authentication successful",
            "api_key_preview": (
                f"{api_key[:8]}...{api_key[-4:]}" if len(api_key) > 12 else "***"
            ),
        }, indent=2))
        sys.exit(EXIT_SUCCESS)

    # Build request
    body = build_google_body(
        args.query, args.mode, args.screenshot, args.follow_ups, args.num,
        region=args.region,
    )
    if args.mode == "ai-mode":
        endpoint = SCRAPE_V2_ENDPOINT
    else:
        endpoint = SCRAPE_V1_ENDPOINT

    # Call API
    result = call_api(api_key, body, endpoint, timeout=args.timeout)

    if result is None:
        _emit_error(
            "NETWORK",
            "API call failed after retries.",
            hint="Check your internet connection and try again.",
            exit_code=EXIT_NETWORK_ERROR,
        )

    result = handle_response(result, api_key, body, endpoint, timeout=args.timeout)

    if result is None:
        _emit_error(
            "NETWORK",
            "API call failed after API key refresh.",
            hint="Check your internet connection and try again.",
            exit_code=EXIT_NETWORK_ERROR,
        )

    # Output
    if args.raw:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        output = extract_google_output(result)
        print(json.dumps(output, indent=2, ensure_ascii=False))

    if result.get("code") != 0:
        sys.exit(EXIT_API_ERROR)

    sys.exit(EXIT_SUCCESS)


if __name__ == "__main__":
    main()


path: scripts/self_test.sh
content: #!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PANGOLIN="$SCRIPT_DIR/pangolin.py"

# Cross-platform Python detection
# On Windows, python3 may exist but exit non-zero; test with --version
if python3 --version &>/dev/null 2>&1; then
  PYTHON="python3"
else
  PYTHON="python"
fi

echo "=== Pangolinfo AI SERP Self-Test ==="

# Test 1: Auth check
echo "[1/2] Testing authentication..."
if $PYTHON "$PANGOLIN" --auth-only; then
  echo "  PASS: Auth OK"
else
  echo "  FAIL: Auth failed (exit code $?)"
  exit 1
fi

# Test 2: Minimal SERP query
echo "[2/2] Testing SERP query..."
if $PYTHON "$PANGOLIN" --q "openclaw" --mode serp 2>/dev/null | $PYTHON -c "import sys,json; d=json.load(sys.stdin); assert d.get('success')==True; print(f'  PASS: Got {d.get(\"results_num\",0)} results')"; then
  :
else
  echo "  FAIL: SERP query failed"
  exit 2
fi

# Error path tests
echo "[E1] Testing missing query error..."
if $PYTHON "$PANGOLIN" 2>/dev/null; then
  echo "  FAIL: Should have errored on missing --q"
  exit 10
else
  echo "  PASS: Correctly rejected missing query"
fi

echo "=== All tests passed ==="


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)