Skill: wangzhiming1999/felo-superagent (version 1.0.2)

--- name: felo-superAgent description: "Felo SuperAgent API: AI conversation with real-time SSE streaming on a persistent LiveDoc canvas. Use when users want SuperAgent chat, continuous conversation, logo/branding design, or e-commerce product images. Do NOT use for tweet/X post writing — use felo-twitter-writer instead. Explicit commands: /felo-superAgent." --- # Felo SuperAgent Skill ## Constraints (MUST READ FIRST) These rules are mandatory. Violating any of them will produce incorrect behavior. 1. **ALWAYS use `--json` flag.** The script MUST run in JSON mode (`--json`). In Claude Code's Bash tool, stdout is always captured — it never streams directly to the user. JSON mode returns the full answer in a structured response that Claude can then output as text. State IDs are extracted from the JSON response fields `thread_short_id` and `live_doc_short_id`. 2. **ALWAYS output the answer directly as text.** After the script finishes, read `data.answer` from the JSON output and print it verbatim as your response text. Do NOT summarize, paraphrase, or add commentary around it. Output it exactly as-is so the user sees the full content. Then, if `data.image_urls` is non-empty, append image links immediately after, formatted as one line per image: `[title](url)`. 3. **`--live-doc-id` is REQUIRED when creating a conversation.** Never call `run_superagent.mjs` without `--live-doc-id`. If you do not have one yet, obtain it first (see Step 2 below). 4. **Reuse `live_doc_id` from ANY source.** If you already have a `live_doc_id` from any previous operation in this session — whether from a prior SuperAgent call, a `felo-livedoc` skill operation, user-provided input, or any other skill — use it directly. Do NOT request the LiveDoc list again. Only fetch the list when no `live_doc_id` is available from any source. (Note: `live_doc_id` corresponds to the API field `live_doc_short_id` and the `[state]` output key `live_doc_short_id`.) 5. **One LiveDoc per session.** All conversations within a session MUST use the same `--live-doc-id`. Do NOT create a new LiveDoc unless the user explicitly asks to "open a new canvas" / "start a new LiveDoc" / "create a new workspace". 6. **Default behavior is follow-up, not new conversation.** After the first question, every subsequent user message is a follow-up. You MUST pass `--thread-id` from the previous response. Only omit `--thread-id` (to start a new thread on the same LiveDoc) when: - The user explicitly says "new topic" / "change subject" / "start over" - The user's intent requires a specific `--skill-id` (e.g., tweet writing, logo design, product image) and the current thread was not created with that skill — because `--skill-id` only takes effect in new conversations 7. **Always persist state.** After every call, extract `thread_short_id` and `live_doc_id` from the stderr `[state]` line (where `live_doc_id` is output as `live_doc_short_id`). Use them in the next call. Losing these IDs breaks conversation continuity. 8. **Skill ID selection (New Conversations Only).** When creating a new conversation (no `--thread-id`), analyze the user's intent and determine if it matches one of the supported skill IDs: **Available skill IDs:** - `twitter-writer` — For composing, drafting, or posting tweets/X posts - `logo-and-branding` — For creating logos, brand designs, or visual identity - `ecommerce-product-image` — For generating product images for e-commerce use **Selection logic:** - If the user explicitly requests a specific skill-id, use their specified value - If the user's intent clearly matches one of the above, pass `--skill-id` with that value - If none of the above match, do NOT pass `--skill-id` (general conversation mode) - `--skill-id` is only effective when creating a new conversation. It is ignored in follow-up mode (`--thread-id`). 9. **Brand style selection for skill-based new conversations.** When starting a NEW conversation that uses a skill ID (`twitter-writer`, `logo-and-branding`, `ecommerce-product-image`), you MUST fetch the style library and let the user choose a style BEFORE calling `run_superagent.mjs`. The chosen style is passed via `--ext '{"brand_style_requirement":"<style_string>"}'`. See Step 4.5 for the full procedure. - The style string is the exact text block output by `run_style_library.mjs` for that entry. Fields vary by category: - **TWITTER**: `Style name` + `Style labels` (language-aware) + `Style DNA` + `Cover file ID` (omitted if null) - **IMAGE**: `Style name` + `Style labels` + `Style DNA` or `Cover file ID` depending on what is present - Use the category that matches the skill: `TWITTER` for `twitter-writer`, `IMAGE` for `logo-and-branding` and `ecommerce-product-image`. - Always pass `--accept-language` to `run_style_library.mjs` so labels are returned in the user's language. - If the user has already specified a style (by name or by pasting the style block), skip the fetch and use their choice directly. - If the style library returns no entries, proceed without `--ext`. - `--ext` is only valid for new conversations. Never pass it in follow-up mode (`--thread-id`). 10. **Never create a new LiveDoc casually.** Reuse the existing one. The only exception is an explicit user request for a new canvas/workspace. ## When to Use Trigger this skill when users want: - **SuperAgent conversation:** AI conversation with Felo SuperAgent, with real-time streaming output - **Continuous conversation:** Multi-turn Q&A on a persistent LiveDoc canvas - **Logo & branding:** Create logos or brand designs (auto-selects `logo-and-branding` skill) - **E-commerce images:** Generate product images (auto-selects `ecommerce-product-image` skill) - **Tool-augmented answers:** Responses that may include image generation, document creation, PPT generation, or Twitter/X search - **Streaming responses:** Real-time answer generation with Server-Sent Events (SSE) **Trigger words:** - English: superagent, super agent, stream chat, streaming conversation, livedoc conversation, continuous chat, follow-up question, create a logo, brand design, product image, e-commerce image - Simplified Chinese (pinyin): chao ji zhu shou, liu shi dui hua, lian xu dui hua, zhui wen, she ji logo, pin pai she ji, dian shang tu pian - Traditional Chinese (pinyin): chao ji zhu shou, liu shi dui hua, lian xu dui hua, zhui wen, she ji logo, pin pai she ji, dian shang tu pian - Japanese (romaji): suupaa eejento, sutoriimingu kaiwa, keizoku kaiwa, rogo sakusei, shouhin gazou **Explicit commands:** `/felo-superAgent`, "use felo superagent", "felo superagent" **Do NOT use for:** - Tweet/X post writing of any kind (use `felo-twitter-writer` instead) - Simple one-off Q&A or real-time information queries (prefer `felo-search`) - Web page content fetching only (use `felo-web-fetch`) - PPT/slide generation only (use `felo-slides`) - LiveDoc knowledge base management only (use `felo-livedoc`) - Twitter/X search only (use `felo-x-search`) ## Setup ### 1. Get Your API Key 1. Visit [felo.ai](https://felo.ai) and log in (or register) 2. Click your avatar in the top right corner → Settings 3. Navigate to the "API Keys" tab 4. Click "Create New Key" to generate a new API Key 5. Copy and save your API Key securely ### 2. Configure API Key The scripts (`run_superagent.mjs`, `run_style_library.mjs`) read the API key **only from the `FELO_API_KEY` environment variable**. The `felo config set` CLI command writes to `~/.felo/config.json` which these scripts do NOT read — environment variable is the only supported method. **Linux/macOS:** ```bash export FELO_API_KEY="your-api-key-here" ``` For permanent configuration, add to your shell profile (`~/.bashrc` or `~/.zshrc`): ```bash echo 'export FELO_API_KEY="your-api-key-here"' >> ~/.zshrc source ~/.zshrc ``` **Windows (PowerShell):** ```powershell $env:FELO_API_KEY="your-api-key-here" ``` **Windows (CMD):** ```cmd set FELO_API_KEY=your-api-key-here ``` ### 3. Dependency: felo-livedoc This skill depends on the `felo-livedoc` skill to obtain and create LiveDocs. Ensure `felo-livedoc/scripts/run_livedoc.mjs` is available at the same level as `felo-superAgent/`. ## How to Execute When this skill is triggered, follow these steps strictly in order. Execute all commands using the Bash tool. ### Step 1: Check API Key ```bash if [ -z "$FELO_API_KEY" ]; then echo "ERROR: FELO_API_KEY not set" exit 1 fi ``` If not set, stop and show the user the setup instructions above. ### Step 2: Obtain `live_doc_id` This step ensures you always have a valid `--live-doc-id` before creating any conversation. (Note: `live_doc_id` corresponds to the API field `live_doc_short_id`.) **2a. If you already have a `live_doc_id` from ANY source in this session:** Skip to Step 3. Reuse the same ID. Sources include: a previous SuperAgent call's `[state]` output (the `live_doc_short_id` field), a `felo-livedoc` skill operation (create, list, etc.), user-provided input, or any other skill that returned a LiveDoc ID. **2b. If no `live_doc_id` is available from any source — fetch the LiveDoc list:** ```bash node felo-livedoc/scripts/run_livedoc.mjs list --json ``` Parse the JSON output. The response contains `data.items` — an array of LiveDoc objects sorted by modification time descending. Find the **first item where `is_shared === false`** and use its `short_id` as your `live_doc_id`. **NEVER pick an item where `is_shared === true`** — shared LiveDocs belong to other projects and will cause a 502 error. Example response: ```json { "status": "ok", "data": { "total": 3, "items": [ { "short_id": "abc123", "name": "Shared Project", "is_shared": true, "modified_at": "..." }, { "short_id": "QPetunwpGnkKuZHStP7gwt", "name": "My Workspace", "is_shared": false, "modified_at": "..." }, ... ] } } ``` Use: `live_doc_id = data.items.find(i => !i.is_shared)?.short_id` **2c. If no `is_shared === false` item exists (or list is empty) — create one:** ```bash node felo-livedoc/scripts/run_livedoc.mjs create --name "SuperAgent Workspace" --json ``` Parse the JSON output and extract `data.short_id` as your `live_doc_id`. Example response: ```json { "status": "ok", "data": { "short_id": "NewDocId123abc", "name": "SuperAgent Workspace", ... } } ``` **2d. If the user explicitly requests a new canvas/workspace:** Create a new LiveDoc (same as 2c), then use the new ID for all subsequent calls. Discard the old `live_doc_id`. ### Step 3: Determine Conversation Mode Decide whether this is a **new conversation** or a **follow-up**: | Condition | Mode | What to pass | | ------------------------------------------------------------ | ---------------- | ------------------------------------------- | | First question in session (no `thread_short_id` yet) | New conversation | `--live-doc-id` only | | User asks a follow-up / continues the topic (DEFAULT) | Follow-up | `--thread-id` AND `--live-doc-id` | | User explicitly says "new topic" / "change subject" | New conversation | `--live-doc-id` only (same LiveDoc) | | User's intent requires a `--skill-id` not matching current thread | New conversation | `--live-doc-id` + `--skill-id` (same LiveDoc) | | User explicitly says "new canvas" / "new LiveDoc" | New conversation | New `--live-doc-id` from Step 2d | **IMPORTANT:** The default for any user message after the first one is ALWAYS follow-up. Only treat it as a new conversation if the user explicitly requests it. ### Step 4: Determine Skill ID (New Conversations Only) If this is a new conversation (no `--thread-id`), analyze the user's intent: **Available skill IDs:** - `twitter-writer` — For composing, drafting, or posting tweets/X posts - `logo-and-branding` — For creating logos, brand designs, or visual identity - `ecommerce-product-image` — For generating product images for e-commerce use **How to decide:** 1. If the user explicitly specifies a skill-id, use that value 2. Otherwise, analyze the user's request and determine if it matches one of the above 3. If none match, do NOT pass `--skill-id` If this is a follow-up (`--thread-id` is set), skip this step entirely. `--skill-id` is ignored in follow-up mode. ### Step 4.5: Fetch and Select Brand Style (New Skill Conversations Only) **When to run this step:** Only when this is a NEW conversation AND a skill ID was determined in Step 4 (`twitter-writer`, `logo-and-branding`, or `ecommerce-product-image`). Skip entirely for follow-up conversations or general (no skill) conversations. **Category mapping:** | Skill ID | Style category | |---|---| | `twitter-writer` | `TWITTER` | | `logo-and-branding` | `IMAGE` | | `ecommerce-product-image` | `IMAGE` | **4.5a. If the user has already specified a style** (by name, or by pasting a style block), use it directly — skip to 4.5d. **4.5b. Fetch the style list (names only):** IMPORTANT: Style DNA content is very large. Always use `--json` and extract only names/labels via Node.js to avoid Bash tool output truncation. Never call `run_style_library.mjs` without `--json` for listing purposes. ```bash # For twitter-writer node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en --json | node -e " const d=require('fs').readFileSync('/dev/stdin','utf8'); const j=JSON.parse(d); const list=j.list||[]; const user=list.filter(s=>!s.recommended); const rec=list.filter(s=>s.recommended); if(user.length){console.log('[Your styles]');user.forEach((s,i)=>{const labels=(s.content?.labels?.en||[]).join(', ');console.log((i+1)+'. '+s.name+(labels?' — '+labels:''));});} if(rec.length){console.log('[Recommended styles]');rec.forEach((s,i)=>{const labels=(s.content?.labels?.en||[]).join(', ');console.log((user.length+i+1)+'. '+s.name+(labels?' — '+labels:''));});} if(!list.length)console.log('(No styles found)'); " # For logo-and-branding or ecommerce-product-image node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language en --json | node -e " const d=require('fs').readFileSync('/dev/stdin','utf8'); const j=JSON.parse(d); const list=j.list||[]; if(list.length){list.forEach((s,i)=>{const labels=(s.content?.labels?.en||s.content?.tags?.en||[]).join(', ');console.log((i+1)+'. '+s.name+(labels?' — '+labels:''));});} else console.log('(No styles found)'); " ``` Replace `en` with the matching `--accept-language` value for the user's language (`zh`, `ja`, `ko`, `en`). Also update the `.labels?.en` reference in the node script to match (e.g. `.labels?.zh` for Chinese). **4.5c. Present the styles to the user and ask them to choose:** Output the COMPLETE list as plain text — every style returned, numbered sequentially. NEVER use the `AskUserQuestion` tool (it limits to 4 options and will silently drop styles). NEVER pre-select or filter styles on behalf of the user. Always append a "no preference" option last. Wait for the user's plain-text reply before proceeding. Example output: ``` Here are the available writing styles — choosing one will make the output more accurate: [Your styles] 1. My Bold Voice — bold, provocative [Recommended styles] 2. darioamodei — Thoughtful long-form essays 3. Casual & Witty — humor, relatable ...(ALL styles listed, none omitted) 0. No preference — use default style ``` If the user picks "no preference" (0) or the list is empty, proceed to Step 5 without `--ext`. **4.5d. Build the `--ext` value:** Take the full text block for the chosen style (exactly as output by the script) and use it as the value of `brand_style_requirement`. The block may contain multiple lines — serialize them into a single JSON string with `\n` for newlines and `\"` for any double quotes inside field values: Example style block output: ``` Style name: darioamodei Style labels: Thoughtful long-form essays Style DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA\n\n## Style Overview\nDario writes like a serious intellectual... ``` Serialized as `--ext`: ```bash --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA\n\n## Style Overview\nDario writes like a serious intellectual..."}' ``` **Important:** Pass the `brand_style_requirement` value completely and verbatim — do NOT truncate `Style DNA`. Partial style content will degrade output quality. ### Step 5: Run the Script Construct and execute the command. **ALWAYS use `--json`** — in Claude Code's Bash tool, stdout is captured, not streamed to the user. JSON mode returns the full answer in a structured response. **IMPORTANT:** The SSE stream may take a long time (especially for image generation, research reports, etc.). You MUST set the Bash tool timeout to at least 600000ms (10 minutes) when executing the script to prevent premature termination. **`--accept-language` selection:** Default is `en`. Match the user's language — if the user writes in Chinese use `zh`, Japanese use `ja`, Korean use `ko`, etc. **`--query` construction:** Do NOT simply pass the user's raw input as-is. You should enrich and refine the query to make it more complete and effective for SuperAgent: - **Add context:** If the conversation has prior context (e.g., the user previously discussed a topic), incorporate relevant details so SuperAgent understands the full picture. - **Clarify vague requests:** If the user says something brief like "continue" or "go on", expand it to describe what should be continued (e.g., "Please continue the previous analysis and provide more details"). - **Supplement missing details:** If the user's request implies information they mentioned earlier (e.g., brand name, product type, style preference), include those details in the query. - **Preserve user intent:** Never change the user's core intent. Only add context and clarity — do not inject opinions or redirect the topic. - **Keep it concise:** The query has a 2000-character limit. Enrich the content but stay focused and avoid unnecessary padding. Examples: - User says "continue" → `--query "Please continue the analysis above on quantum computing, expanding on real-world applications"` - User says "one more" → `--query "Please generate another product image in a similar style, white background"` - User says "fix it" → `--query "Please revise the tweet generated above, make the tone more casual and add some emojis"` **New conversation (first question, no skill):** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "USER_QUERY_HERE" \ --live-doc-id "LIVE_DOC_ID" \ --accept-language en \ --json ``` **New conversation with skill ID, no style selected:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Write a tweet about the latest AI trends" \ --live-doc-id "LIVE_DOC_ID" \ --skill-id twitter-writer \ --accept-language en \ --json ``` **New conversation with skill ID and brand style (from Step 4.5):** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Write a tweet about the latest AI trends" \ --live-doc-id "LIVE_DOC_ID" \ --skill-id twitter-writer \ --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA\n\n## Style Overview\nDario writes like a serious intellectual...（full content）"}' \ --accept-language en \ --json ``` **Follow-up question (DEFAULT for 2nd+ messages):** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "USER_FOLLOW_UP_QUERY" \ --thread-id "THREAD_SHORT_ID_FROM_PREVIOUS" \ --live-doc-id "LIVE_DOC_ID" \ --json ``` ### Step 6: Extract State and Output the Answer After the script finishes, parse the JSON output: ```json { "status": "ok", "data": { "answer": "...", "thread_short_id": "CmYpuGwBgCnrUdDx5ZtmxA", "live_doc_short_id": "QPetunwpGnkKuZHStP7gwt", "live_doc_url": "https://felo.ai/livedoc/QPetunwpGnkKuZHStP7gwt", "image_urls": [ { "url": "https://...", "title": "Image title", "file_id": [REDACTED_SECRET] } ] } } ``` 1. **Output `data.answer` verbatim** as your response text — print it exactly as-is so the user sees the full content. 2. **Extract and save** `data.thread_short_id` and `data.live_doc_short_id` — you MUST use these in the next call. 3. **Optionally show** `data.live_doc_url` so the user can view the LiveDoc canvas in a browser. 4. **Image results (`data.image_urls`):** If this array is non-empty, append image links immediately after `data.answer`, formatted as **one line per image**: ``` [title](url) ``` Example output: ``` [Giant panda eating bamboo](https://...) [Giant panda dancing](https://...) [Blue whale leaping out of the water](https://...) ``` Each image has `url` (signed S3 URL, time-limited), `title`, and `file_id` (stable file identifier). Note: the same image may appear in both `tools_result_stream` and `tools_result` events with different signed URLs — deduplication is handled automatically by `file_id`. When referencing a previously generated image in a follow-up query, include its `file_id` in the `--query` so SuperAgent can locate the file (e.g., `"Please generate a variation of file_id=b9e5be11-..."`). Do NOT show `thread_short_id` or `live_doc_short_id` to the user unless they ask for it. ## Complete Workflow Examples ### Example A: Multi-turn Conversation (Most Common) ``` User: "What is quantum computing?" ``` **Step 2b:** Fetch LiveDoc list → get `live_doc_id = "QPetunwpGnkKuZHStP7gwt"` **Step 3:** First question → New conversation **Step 4:** No special skill → no `--skill-id` **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "What is quantum computing?" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --accept-language en \ --json ``` **Step 6:** Parse JSON output. Output `data.answer` verbatim as your response. Save `thread_short_id = "CmYpuGwBgCnrUdDx5ZtmxA"`, `live_doc_id = "QPetunwpGnkKuZHStP7gwt"` from `data`. ``` User: "What are its practical applications?" ``` **Step 2a:** Already have `live_doc_id` → skip **Step 3:** Follow-up (default) → use saved `thread_short_id` **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "What are its practical applications?" \ --thread-id "CmYpuGwBgCnrUdDx5ZtmxA" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --json ``` **Step 6:** Parse JSON output. Output `data.answer` verbatim. Save updated `thread_short_id` from `data` (may be the same), keep `live_doc_id`. ``` User: "Tell me more about quantum error correction" ``` **Step 3:** Still follow-up (same topic) → use saved `thread_short_id` **Step 5:** Same pattern as above with new query ### Example B: Tweet Writing with Style Selection ``` User: "Help me write a tweet about AI trends" ``` **Step 2a:** Already have `live_doc_id` → reuse **Step 3:** New conversation **Step 4:** User intent matches "write a tweet" → `--skill-id twitter-writer` **Step 4.5:** Fetch TWITTER styles (pass `--accept-language` matching user's language): ```bash node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en ``` Output: ``` Style name: My Bold Voice Style labels: bold, provocative Style DNA: # My Bold Voice Style DNA ...（full content） Style name: darioamodei Style labels: Thoughtful long-form essays Style DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA ...（full content） ``` Present to user: "Which writing style would you like? 1. My Bold Voice (yours) 2. darioamodei (recommended) 3. No preference" User selects: "1. My Bold Voice" **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Help me write a tweet about AI trends" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --skill-id twitter-writer \ --ext '{"brand_style_requirement":"Style name: My Bold Voice\nStyle labels: bold, provocative\nStyle DNA: # My Bold Voice Style DNA\n...（full content）"}' \ --accept-language en \ --json ``` **Step 6:** Parse JSON output. Output `data.answer` verbatim. Save new `thread_short_id` from `data`, keep same `live_doc_id`. ``` User: "Make it more casual and add some emojis" ``` **Step 3:** Follow-up → use saved `thread_short_id` (do NOT pass `--ext` again) **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Make it more casual and add some emojis" \ --thread-id "NEW_THREAD_FROM_TWEET" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --json ``` ### Example C: Logo Design with Style Selection ``` User: "Design a logo for my coffee shop called Bean & Brew" ``` **Step 4:** Detected "design a logo" → `--skill-id logo-and-branding` **Step 4.5:** Fetch IMAGE styles: ```bash node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language en ``` Output example: ``` Style name: Minimalist Modern Style labels: clean, monochrome Style DNA: ...（full content） Cover file ID: file_333 ``` Present styles to user and wait for selection. Suppose user picks "Minimalist Modern": **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Design a logo for my coffee shop called Bean & Brew" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --skill-id logo-and-branding \ --ext '{"brand_style_requirement":"Style name: Minimalist Modern\nStyle labels: clean, monochrome\nStyle DNA: ...（full content）\nCover file ID: file_333"}' \ --accept-language en \ --json ``` ### Example D: E-commerce Product Image with Style Selection ``` User: "Generate a product image for a wireless headphone on white background" ``` **Step 4:** Detected "product image" → `--skill-id ecommerce-product-image` **Step 4.5:** Fetch IMAGE styles: ```bash node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language en ``` Present styles to user. Suppose user picks "No preference": **Step 5:** (no `--ext` since user chose no preference) ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Generate a product image for a wireless headphone on white background" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --skill-id ecommerce-product-image \ --accept-language en \ --json ``` ### Example E: User Requests a New Canvas ``` User: "Open a new canvas for a different project" ``` **Step 2d:** Create new LiveDoc: ```bash node felo-livedoc/scripts/run_livedoc.mjs create --name "New Project" --json ``` Extract new `live_doc_id`. Discard the old one. All subsequent calls use the new ID. ### Example F: User Specifies Style Directly ``` User: "Write a tweet about AI trends using the 'darioamodei' style" ``` **Step 4:** `--skill-id twitter-writer` **Step 4.5a:** User already named the style → fetch the list to get the full block: ```bash node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en ``` Find the entry with `Style name: darioamodei`, extract its full block verbatim. No need to ask the user again. **Step 5:** ```bash node felo-superAgent/scripts/run_superagent.mjs \ --query "Write a tweet about AI trends" \ --live-doc-id "QPetunwpGnkKuZHStP7gwt" \ --skill-id twitter-writer \ --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA\n\n## Style Overview\nDario writes like a serious intellectual...（full content, do NOT truncate）"}' \ --accept-language en \ --json ``` ## Available Script Options **Core parameters:** - `--query <text>` (REQUIRED) — User question, 1-2000 characters - `--live-doc-id <id>` (REQUIRED for new conversations) — LiveDoc ID (`live_doc_id`) to associate with - `--thread-id <id>` — Thread ID from previous response, for follow-up conversations **Skill parameters (new conversations only, ignored in follow-up):** - `--skill-id <id>` — Skill ID (see Constraint #9 for available skill IDs) - `--selected-resource-ids <ids>` — Comma-separated resource IDs to include - `--ext <json>` — Extra parameters as JSON object. For skill-based conversations, pass brand style as: `--ext '{"brand_style_requirement":"Style name: <name>\nStyle labels: <labels>\nStyle DNA: <full styleDna text>\nCover file ID: <id>"}'` Fields present depend on category type. `Cover file ID` is omitted when null. Do NOT truncate `Style DNA`. **Output control:** - `--json` / `-j` — Output JSON format with full metadata (ALWAYS use this in Claude Code — stdout is captured by the Bash tool, not streamed to the user) - `--verbose` / `-v` — Log stream connection details to stderr (for debugging only, not needed for normal use) - `--accept-language <lang>` — Language preference (e.g., en, ja, ko) ## API Workflow (Reference) The script handles this workflow automatically: 1. **Create conversation:** - New: `POST /v2/conversations` (requires `live_doc_short_id` in body) - Follow-up: `POST /v2/conversations/{threadId}/follow_up` - Returns: `stream_key`, `thread_short_id`, `live_doc_short_id` 2. **Consume SSE stream:** - `GET /v2/conversations/stream/{stream_key}` - Supports offset parameter for resuming: `?offset={lastOffset}` - Reconnects automatically if connection drops (2-second delay) 3. **Parse events:** - `message` — Direct text content - `stream` — Wrapped content with type information - `heartbeat` — Keep-alive signal - `done` / `completed` / `complete` — Stream finished - `error` — Error event (non-terminal, continues reading) 4. **Extract tool results:** - Image generation (`generate_images`) - Research reports (`generate_discovery`) - Document generation (`generate_document`) - PPT generation (`generate_ppt`) - HTML generation (`generate_html`) - Twitter/X search (`search_x`) Base URL: `https://openapi.felo.ai` (override with `FELO_API_BASE` if needed). ## Tool Support SuperAgent may invoke tools during conversation. The script automatically extracts and displays: **Image Generation:** - Tool: `generate_images` - Output: Image URLs and titles **Research & Discovery:** - Tool: `generate_discovery` - Output: Research report titles and status **Document Generation:** - Tool: `generate_document` - Output: Document titles and status **Presentation Generation:** - Tool: `generate_ppt` - Output: PPT titles and status **HTML Generation:** - Tool: `generate_html` - Output: HTML page titles and status **Twitter/X Search:** - Tool: `search_x` - Output: Tweet content, author info, metrics (likes, retweets, views) ## Error Handling ### Common Error Codes | Code | HTTP | Description | | -------------------------------------- | ---- | ------------------------------------------------- | | INVALID_API_KEY | 401 | API Key is invalid or has been revoked | | SUPER_AGENT_CONVERSATION_CREATE_FAILED | 502 | Failed to create conversation (upstream error) | | SUPER_AGENT_CONVERSATION_QUERY_FAILED | 502 | Failed to query conversation details | ### SSE Stream Errors The stream may send: - `event: error` with `data: {"message": "..."}` — treat as failure and show message - Connection timeout — script automatically reconnects with 2-second delay - Idle timeout (2 hours) — stream aborted if no data received ### Missing API Key If `FELO_API_KEY` is not set, display this message: ``` ERROR: FELO_API_KEY not set To use this skill, you need to set up your Felo API Key: 1. Get your API key from https://felo.ai (Settings -> API Keys) 2. Set the environment variable: Linux/macOS: export FELO_API_KEY="your-api-key-here" Windows (PowerShell): $env:FELO_API_KEY="your-api-key-here" 3. Restart Claude Code or reload the environment ``` ### Timeout Handling - The SSE stream has its own idle timeout: 2 hours (no data received). The stream stays open as long as data keeps flowing. - **Bash tool timeout:** MUST be set to at least 600000ms (10 minutes) when executing the script, because the SSE stream can run for a long time. ## Important Notes - Execute this skill immediately using the Bash tool — do not just describe what you would do - **ALWAYS use `--json`** — in Claude Code's Bash tool, stdout is captured, not streamed. JSON mode returns the answer in a structured response that Claude outputs as text - **ALWAYS output `data.answer` verbatim** — print it exactly as-is as your response text so the user sees the full content - After create, the script connects to the stream **immediately** — the `stream_key` has a limited validity period - Use the bundled Node script to consume SSE; do not assume `jq` or other tools for parsing SSE - Same API key as other Felo skills (`FELO_API_KEY`) - The script handles reconnection automatically if the stream drops - Tool results are deduplicated to avoid showing the same resource multiple times - If `live_doc_id` is already known from any source (other skills, user input, previous calls), use it directly — do NOT fetch the LiveDoc list again - Multi-language support: Fully supports Simplified Chinese, Traditional Chinese, Japanese, and English - The API returns results in the same language as the query when possible ## Decision Flowchart ``` User sends a message | v Have live_doc_id from ANY source? NO --> Step 2b: fetch list --> got is_shared=false item? YES --> use data.items.find(i => !i.is_shared)?.short_id as live_doc_id NO --> Step 2c: create new LiveDoc YES --> continue (reuse it, do NOT fetch list) | v Have thread_short_id from previous call? NO --> This is a NEW conversation --> Step 4: determine skill-id by analyzing user intent --> skill-id found (twitter-writer / logo-and-branding / ecommerce-product-image)? YES --> Step 4.5: fetch style library for matching category --> styles available? YES --> present to user, wait for selection --> user picked a style? YES --> build --ext '{"brand_style_requirement":"..."}' NO --> no --ext NO --> no --ext NO --> no --ext, no --skill-id --> Step 5: run WITHOUT --thread-id (with --skill-id and --ext if determined above) YES --> Does user's intent require a skill-id not matching current thread? YES --> NEW conversation (same live-doc-id, with --skill-id, repeat Step 4.5) NO --> Is user explicitly starting a new topic? YES --> NEW conversation (same live-doc-id, no --thread-id) NO --> FOLLOW-UP (pass --thread-id, NO --ext) | v Run script (WITH --json, Bash timeout >= 600000ms) --> parse JSON, output data.answer verbatim | v Extract thread_short_id + live_doc_id from stderr [state] line | v Do NOT repeat or summarize the answer (already shown) ``` ## Style Library Script (`run_style_library.mjs`) Fetch the style library list for a given category. Returns user styles first, then recommended styles. ```bash node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en ``` **Options:** - `--category <category>` (REQUIRED) — One of: `TWITTER`, `INSTAGRAM`, `LEMON8`, `NOTECOM`, `WEBSITE`, `IMAGE` - `--accept-language <lang>` — Language for labels/tags (e.g. `en`, `zh-Hans`, `ja`). Default: `en`. Always pass this to match the user's language. - `--json` / `-j` — Output raw JSON - `--timeout <seconds>` — Request timeout (default 60) **Default text output format (one block per style, blank line between):** For TWITTER category: ``` Style name: darioamodei Style labels: Thoughtful long-form essays Style DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA ...（full styleDna content） ``` Fields included per entry (fields with null/empty values are omitted): - `Style name` — always present (`name` field) - `Style labels` — from `content.labels` (TWITTER) or `content.tags` (other categories), in the requested language, comma-separated; omitted if not present - `Style DNA` — from `content.styleDna` (TWITTER type); omitted if not present - `Cover file ID` — from `coverFileId`; omitted if null/empty User-created styles appear before recommended styles. ## References - [SuperAgent API (Felo Open Platform)](https://openapi.felo.ai/docs/api-reference/v2/superagent.html) - [Felo Open Platform](https://openapi.felo.ai/docs/) - [Get API Key](https://felo.ai) (Settings -> API Keys)

path: clawhub.json
content: {
  "name": "Felo SuperAgent",
  "tagline": "AI conversation with real-time SSE streaming and LiveDoc in Claude Code",
  "description": "Felo SuperAgent lets you chat with Felo SuperAgent via the Open API from Claude Code: create sessions, get SSE streamed replies, and continue conversations on the same LiveDoc. Supports thread/LiveDoc management, multi-language, and tools (image gen, reports, PPT, Twitter search). Requires a Felo API key.",
  "category": "conversation",
  "tags": ["felo", "superagent", "chat", "streaming", "livedoc", "api", "sse"],
  "version": "1.0.0",
  "license": "MIT",
  "pricing": "free",
  "support_url": "https://github.com/Felo-Inc/felo-skills/issues",
  "homepage": "https://github.com/Felo-Inc/felo-skills",
  "dependencies": ["felo-livedoc"]
}


path: README.md
content: # Felo SuperAgent Skill for Claude Code

**AI conversation with real-time streaming output, supporting continuous multi-turn conversation.**

Use the Felo Open Platform SuperAgent API in Claude Code to initiate conversations with SuperAgent, receive real-time SSE streaming responses, and manage conversation state across turns.

---

## Features

- **Streaming conversation** — create a conversation and receive AI responses in real-time via SSE
- **LiveDoc association** — each conversation is linked to a LiveDoc for resource tracking
- **Continuous conversation** — continue asking questions in an existing thread using `--thread-id`
- **Brand style support** — pass a writing style via `--ext` to guide output (used by `felo-twitter-writer` and other skills)
- **Style library** — fetch saved brand styles from the API with `run_style_library.mjs`
- **LiveDoc management** — list LiveDocs and view resources within a specific LiveDoc
- **Multi-language** — supports `--accept-language` (e.g., zh, en, ja, ko)
- **Tool invocation** — image generation, research reports, documents, PPT, HTML, Twitter/X search, and more

**Use cases:**

- Need SuperAgent streaming answers
- Need conversation associated with LiveDoc for traceable resources
- Multi-turn/continuous conversation (reuse the same LiveDoc)
- Tweet writing, logo design, or e-commerce product images with brand style guidance

**Not suitable for:**

- Simple one-off real-time information retrieval → use `felo-search`
- Only need to fetch webpage content → use `felo-web-fetch`
- Only need to generate PPT → use `felo-slides`
- Need LiveDoc knowledge base features → use `felo-livedoc`

---

## Quick Start

### 1. Installation

**Via ClawHub:**

```bash
clawhub install felo-superAgent
```

**Manual install:**

```bash
# Linux/macOS
cp -r felo-superAgent ~/.claude/skills/

# Windows (PowerShell)
Copy-Item -Recurse felo-superAgent "$env:USERPROFILE\.claude\skills\"
```

### 2. Configure API Key

1. Open [felo.ai](https://felo.ai) and log in
2. Avatar → **Settings** → **API Keys** → Create and copy key
3. Set environment variable:

```bash
# Linux/macOS
export FELO_API_KEY="your-api-key-here"

# Windows PowerShell
$env:FELO_API_KEY="your-api-key-here"
```

For permanent configuration, add to your shell profile (`~/.bashrc`, `~/.zshrc`) or system environment variables.

### 3. Usage

**Trigger in Claude Code conversation:**

```
/felo-superagent What is the latest news about AI?
/felo-superagent Tell me more
```

**Run script directly:**

```bash
node felo-superAgent/scripts/run_superagent.mjs --query "What is quantum computing?"
node felo-superAgent/scripts/run_superagent.mjs --query "Tell me more" --thread-id <thread_short_id>
```

**CLI commands (after `npm install -g felo-ai`):**

```bash
felo superagent "What is the latest news about AI?"
felo superagent "Tell me more" --thread-id <thread_short_id>
felo livedocs
felo livedoc-resources <livedoc-id>
felo style-library TWITTER --accept-language en
```

---

## Scripts

This skill provides two scripts:

| Script | Description |
|---|---|
| `run_superagent.mjs` | Create/continue a conversation and stream the answer |
| `run_style_library.mjs` | Fetch brand styles from the style library API |

### run_superagent.mjs parameters

| Parameter | Description |
|---|---|
| `--query <text>` | User question (required, 1–2000 characters) |
| `--thread-id <id>` | Existing thread ID for follow-up conversations |
| `--live-doc-id <id>` | Reuse existing LiveDoc short_id |
| `--skill-id <id>` | Skill ID for new conversations (`twitter-writer`, `logo-and-branding`, `ecommerce-product-image`) |
| `--selected-resource-ids <ids>` | Comma-separated resource IDs (new conversations only) |
| `--ext <json>` | Extra parameters as JSON (new conversations only). Used for brand style — see below. |
| `--accept-language <lang>` | Language preference: `en`, `zh`, `ja`, `ko`, etc. |
| `--timeout <seconds>` | Request/stream timeout, default 60 |
| `--json` | Output JSON with `answer`, `thread_short_id`, `live_doc_short_id` |
| `--verbose` | Log stream connection details to stderr |

### run_style_library.mjs parameters

| Parameter | Description |
|---|---|
| `--category <category>` | Style category (required): `TWITTER`, `INSTAGRAM`, `LEMON8`, `NOTECOM`, `WEBSITE`, `IMAGE` |
| `--accept-language <lang>` | Language for labels/tags (e.g. `en`, `zh-Hans`, `ja`). Default: `en` |
| `--json` | Output raw JSON |
| `--timeout <seconds>` | Request timeout, default 60 |

---

## Brand Style (`--ext`)

When starting a new conversation with a skill ID, you can pass a brand style via `--ext` to guide the output. The style is fetched from the style library and serialized as a JSON string.

### Fetch styles

```bash
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en
```

Output (one block per style, blank line between):

```
Style name: darioamodei
Style labels: Thoughtful long-form essays
Style DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA
...（full content）

Style name: My Bold Voice
Style labels: bold, provocative
Style DNA: # My Bold Voice Style DNA
...（full content）
Cover file ID: file_abc123
```

Fields per entry:

| Field | Source | Notes |
|---|---|---|
| `Style name` | `name` | Always present |
| `Style labels` | `content.labels[lang]` or `content.tags[lang]` | Language-aware, comma-separated; omitted if absent |
| `Style DNA` | `content.styleDna` | Full text (TWITTER type); omitted if absent |
| `Cover file ID` | `coverFileId` | Omitted if null |

User-created styles appear before recommended styles.

### Pass style to SuperAgent

Take the full text block for the chosen style and serialize it into `brand_style_requirement`. Pass the value **completely — do NOT truncate `Style DNA`**:

```bash
node felo-superAgent/scripts/run_superagent.mjs \
  --query "Write a tweet about AI trends" \
  --live-doc-id "LIVE_DOC_ID" \
  --skill-id twitter-writer \
  --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA\n\n## Style Overview\nDario writes like a serious intellectual...（full content）"}' \
  --accept-language en
```

Category mapping for skill IDs:

| Skill ID | Style category |
|---|---|
| `twitter-writer` | `TWITTER` |
| `logo-and-branding` | `IMAGE` |
| `ecommerce-product-image` | `IMAGE` |

`--ext` is only valid for new conversations. Never pass it in follow-up mode (`--thread-id`).

---

## Using with Claude Code

### Installation

```bash
# Via ClawHub
clawhub install felo-superAgent

# Manual
cp -r felo-superAgent ~/.claude/skills/
```

### Triggering the skill

Claude Code automatically triggers this skill for SuperAgent conversations. Explicit commands:

```
/felo-superagent What is the latest news about AI?
/felo-superagent Write a tweet about AI trends
/felo-superagent Create a logo for my coffee shop
```

### What Claude manages automatically

- **LiveDoc reuse** — reuses the same LiveDoc across the session; only fetches the list when no ID is available
- **Thread continuity** — passes `--thread-id` for all follow-up messages; only starts a new thread when the user explicitly requests it or a different skill ID is needed
- **Style selection** — for skill-based new conversations (`twitter-writer`, `logo-and-branding`, `ecommerce-product-image`), fetches the matching style library, presents options to the user, and passes the chosen style via `--ext`
- **State extraction** — after every call, extracts `thread_short_id` and `live_doc_short_id` from the stderr `[state]` line for use in the next call

### Example conversation

```
You:    Write a tweet about AI trends

Claude: Here are the available Twitter writing styles — choosing one will make
        the output more accurate:

        [Your styles]
        1. My Bold Voice

        [Recommended styles]
        2. darioamodei

        0. No preference — use default style

You:    2

Claude: [streams the tweet in darioamodei style in real time]

You:    Make it shorter

Claude: [follow-up — no style re-selection, streams updated tweet]
```

---

## Output Format

**Default (streaming plain text):**
Answer streams directly to stdout in real time. Do not summarize or re-output it.

**`--json` (suppresses streaming):**

```json
{
  "status": "ok",
  "data": {
    "answer": "Complete answer content...",
    "thread_short_id": "TvyKouzJirXjFdst4uKRK3",
    "live_doc_short_id": "PvyKouzJirXjFdst4uKRK3",
    "live_doc_url": "https://felo.ai/livedoc/...",
    "image_urls": [{"url": "...", "title": "..."}],
    "discoveries": [{"title": "Research Report"}],
    "documents": [{"title": "Generated Document"}],
    "ppts": [{"title": "Presentation"}],
    "htmls": [{"title": "HTML Page"}],
    "search_x": [{"tweets": [...]}]
  }
}
```

**State line (stderr):**

```
[state] thread_short_id=TvyKouzJirXjFdst4uKRK3 live_doc_short_id=PvyKouzJirXjFdst4uKRK3 live_doc_url=https://felo.ai/livedoc/...
```

Always extract `thread_short_id` and `live_doc_short_id` from this line for use in the next call.

---

## Complete Examples

### Example 1: Simple question

```bash
node felo-superAgent/scripts/run_superagent.mjs \
  --query "What is quantum computing?" \
  --accept-language en
```

### Example 2: Follow-up in thread

```bash
# First question — capture thread_short_id from stderr [state]
node felo-superAgent/scripts/run_superagent.mjs \
  --query "What is machine learning?" \
  --live-doc-id "PvyKouzJirXjFdst4uKRK3"

# Follow-up
node felo-superAgent/scripts/run_superagent.mjs \
  --query "Can you elaborate on neural networks?" \
  --thread-id "TvyKouzJirXjFdst4uKRK3" \
  --live-doc-id "PvyKouzJirXjFdst4uKRK3"
```

### Example 3: Tweet writing with brand style

```bash
# Fetch styles
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en

# New conversation with chosen style
node felo-superAgent/scripts/run_superagent.mjs \
  --query "Write a tweet about AI trends" \
  --live-doc-id "PvyKouzJirXjFdst4uKRK3" \
  --skill-id twitter-writer \
  --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: ...（full content）"}' \
  --accept-language en
```

### Example 4: Logo design with brand style

```bash
# Fetch IMAGE styles
node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language en

# New conversation with chosen style
node felo-superAgent/scripts/run_superagent.mjs \
  --query "Design a logo for my coffee shop called Bean & Brew" \
  --live-doc-id "PvyKouzJirXjFdst4uKRK3" \
  --skill-id logo-and-branding \
  --ext '{"brand_style_requirement":"Style name: Minimalist Modern\nStyle labels: clean, monochrome\nStyle DNA: ...（full content）\nCover file ID: file_333"}' \
  --accept-language en
```

---

## Tool Support

SuperAgent may invoke tools during conversation. The script automatically extracts and displays:

| Tool | Output |
|---|---|
| `generate_images` | Image URLs and titles |
| `generate_discovery` | Research report titles |
| `generate_document` | Document titles |
| `generate_ppt` | PPT titles |
| `generate_html` | HTML page titles |
| `search_x` | Tweet content, author info, metrics |

---

## Error Handling

| Error | Cause | Solution |
|---|---|---|
| `FELO_API_KEY not set` | Missing API key | Set `FELO_API_KEY` env var |
| `HTTP 401: INVALID_API_KEY` | Invalid or revoked key | Generate a new key at felo.ai |
| [REDACTED_SECRET] (502) | Upstream error | Retry; contact support if persistent |
| Stream idle timeout | No data for 2 hours | Retry the query |
| Connection hangs | Firewall blocking SSE | Try `--verbose`; check proxy settings |

---

## Advanced Usage

### Custom API base URL

```bash
export FELO_API_BASE="https://custom-api.example.com"
node felo-superAgent/scripts/run_superagent.mjs --query "test"
```

### Resource selection

```bash
node felo-superAgent/scripts/run_superagent.mjs \
  --query "Summarize these documents" \
  --live-doc-id "PvyKouzJirXjFdst4uKRK3" \
  --selected-resource-ids "res1,res2,res3"
```

---

## References

- [SKILL.md](SKILL.md) — full agent instructions, decision logic, and style library format
- [SuperAgent API Documentation](https://openapi.felo.ai/docs/api-reference/v2/superagent.html)
- [Felo Open Platform](https://openapi.felo.ai/docs/)
- [Get API Key](https://felo.ai) (Settings → API Keys)
- [GitHub Repository](https://github.com/Felo-Inc/felo-skills)



path: scripts/run_style_library.mjs
content: #!/usr/bin/env node

const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_TIMEOUT_SEC = 60;

const VALID_CATEGORIES = ['TWITTER', 'INSTAGRAM', 'LEMON8', 'NOTECOM', 'WEBSITE', 'IMAGE'];

function usage() {
  console.error(
    [
      'Usage:',
      '  node felo-superAgent/scripts/run_style_library.mjs --category <category> [options]',
      '',
      'Options:',
      '  --category <category>        Style category (required)',
      `                               One of: ${VALID_CATEGORIES.join(', ')}`,
      '  --accept-language <lang>     Language for labels/tags (e.g. en, zh-Hans, ja). Default: en',
      '  --json                       Output raw JSON',
      '  --timeout <seconds>          Request timeout, default 60',
      '  --help                       Show this help',
    ].join('\n')
  );
}

function parseArgs(argv) {
  const out = {
    category: '',
    acceptLanguage: 'en',
    json: false,
    timeoutSec: DEFAULT_TIMEOUT_SEC,
    help: false,
  };

  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--help' || a === '-h') {
      out.help = true;
    } else if (a === '--json' || a === '-j') {
      out.json = true;
    } else if (a === '--category' || a === '-c') {
      out.category = (argv[++i] || '').trim().toUpperCase();
    } else if (a === '--accept-language') {
      out.acceptLanguage = (argv[++i] || 'en').trim();
    } else if (a === '--timeout' || a === '-t') {
      const n = parseInt(argv[++i] || '', 10);
      if (Number.isFinite(n) && n > 0) out.timeoutSec = n;
    }
  }

  return out;
}

function getMessage(payload) {
  return payload?.message || payload?.error || payload?.msg || payload?.code || 'Unknown error';
}

function isApiError(payload) {
  const status = payload?.status;
  const code = payload?.code;
  if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
  if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
  return false;
}

/**
 * Pick the best matching value from a multilingual map object.
 * Falls back through: exact match → base language (e.g. "zh" for "zh-Hans") → "en" → first available.
 * Returns an array (may be empty).
 */
function pickLangValue(map, lang) {
  if (!map || typeof map !== 'object') return [];
  if (map[lang]) return map[lang];
  // Try base language (e.g. "zh" from "zh-Hans")
  const base = lang.split('-')[0];
  if (base !== lang && map[base]) return map[base];
  // Fallback to English
  if (map['en']) return map['en'];
  // Last resort: first available key
  const first = Object.values(map)[0];
  return Array.isArray(first) ? first : [];
}

/**
 * Format a single style entry into the brand_style_requirement string.
 * Fields included (null/empty fields are omitted):
 *   Style name: <name>
 *   Style labels: <label1, label2>   (from content.labels, language-aware)
 *   Style DNA: <styleDna>            (from content.styleDna, TWITTER type)
 *   Cover file ID: <coverFileId>     (omitted if null/empty)
 */
function formatStyle(s, lang) {
  const lines = [];

  // Style name (always present)
  lines.push(`Style name: ${s.name ?? ''}`);

  const content = s.content ?? {};

  // Style labels — multilingual map (content.labels for TWITTER, content.tags for others)
  const labelsMap = content.labels ?? content.tags ?? null;
  if (labelsMap) {
    const labelArr = pickLangValue(labelsMap, lang);
    if (labelArr.length > 0) {
      lines.push(`Style labels: ${labelArr.join(', ')}`);
    }
  }

  // Style DNA (TWITTER type)
  if (typeof content.styleDna === 'string' && content.styleDna.trim()) {
    lines.push(`Style DNA: ${content.styleDna.trim()}`);
  }

  // Cover file ID — omit if null/empty
  const coverId = s.coverFileId ?? s.cover_file_id ?? null;
  if (coverId) {
    lines.push(`Cover file ID: ${coverId}`);
  }

  return lines.join('\n');
}

async function main() {
  const args = parseArgs(process.argv.slice(2));

  if (args.help) {
    usage();
    process.exit(0);
  }

  if (!args.category) {
    console.error('Error: --category is required');
    usage();
    process.exit(1);
  }

  if (!VALID_CATEGORIES.includes(args.category)) {
    console.error(`Error: invalid category "${args.category}". Must be one of: ${VALID_CATEGORIES.join(', ')}`);
    process.exit(1);
  }

  const [REDACTED_SECRET]);
  if (!apiKey) {
    console.error(
      'ERROR: FELO_API_KEY not set\n\n' +
      'To use this script, set FELO_API_KEY:\n' +
      '  export FELO_API_KEY="your-api-key-here"\n' +
      'Get your API key from https://felo.ai (Settings -> API Keys).'
    );
    process.exit(1);
  }

  const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
  const timeoutMs = args.timeoutSec * 1000;

  const params = new URLSearchParams();
  params.set('category', args.category);
  const url = `${apiBase}/v2/brand/style-library/list?${params.toString()}`;

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, {
      method: 'GET',
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${apiKey}`,
      },
      signal: controller.signal,
    });

    let data = {};
    try {
      data = await res.json();
    } catch {
      data = {};
    }

    if (!res.ok) {
      throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
    }
    if (isApiError(data)) {
      throw new Error(getMessage(data));
    }

    const list = data?.data?.list ?? [];

    if (args.json) {
      console.log(JSON.stringify(data?.data ?? {}, null, 2));
    } else {
      if (list.length === 0) {
        console.log('(No styles found)');
      } else {
        // User styles first, then recommended styles
        const userStyles = list.filter((s) => !s.recommended);
        const recommendedStyles = list.filter((s) => s.recommended);
        const allFormatted = [...userStyles, ...recommendedStyles].map((s) => formatStyle(s, args.acceptLanguage));
        console.log(allFormatted.join('\n\n'));
      }
    }
  } catch (err) {
    if (err?.name === 'AbortError') {
      console.error(`Error: Request timed out after ${timeoutMs / 1000}s`);
    } else {
      console.error(`Error: ${err?.message || err}`);
    }
    process.exit(1);
  } finally {
    clearTimeout(timer);
  }
}

main();


path: scripts/run_superagent.mjs
content: #!/usr/bin/env node

const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_TIMEOUT_SEC = 60;
const STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
const RECONNECT_DELAY_MS = 2000;
const HIDDEN_TOOLS = new Set(['manage_outline']);

function usage() {
  console.error(
    [
      'Usage:',
      '  node felo-superAgent/scripts/run_superagent.mjs --query "your question" [options]',
      '',
      'Options:',
      '  --query <text>                 User question (required, 1-2000 chars)',
      '  --thread-id <id>              Existing thread ID for follow-up',
      '  --live-doc-id <id>            Reuse existing LiveDoc short_id',
      '  --skill-id <id>               Skill ID (new conversations only)',
      '  --selected-resource-ids <ids> Comma-separated resource IDs (new conversations only)',
      '  --ext <json>                  Extra params JSON (new conversations only)',
      '  --accept-language <lang>      Language preference (e.g. zh, en)',
      '  --timeout <seconds>           Request/stream timeout, default 60',
      '  --json                        Output JSON with answer, thread_short_id, live_doc_short_id',
      '  --verbose                     Log stream details to stderr',
      '  --help                        Show this help',
    ].join('\n')
  );
}

function parseArgs(argv) {
  const out = {
    query: '',
    threadId: '',
    liveDocId: '',
    skillId: '',
    selectedResourceIds: [],
    ext: null,
    acceptLanguage: '',
    timeoutSec: DEFAULT_TIMEOUT_SEC,
    json: false,
    verbose: false,
    help: false,
  };

  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--help' || a === '-h') {
      out.help = true;
    } else if (a === '--json' || a === '-j') {
      out.json = true;
    } else if (a === '--verbose' || a === '-v') {
      out.verbose = true;
    } else if (a === '--query' || a === '-q') {
      out.query = (argv[++i] || '').trim();
    } else if (a === '--thread-id') {
      out.threadId = (argv[++i] || '').trim();
    } else if (a === '--live-doc-id') {
      out.liveDocId = (argv[++i] || '').trim();
    } else if (a === '--skill-id') {
      out.skillId = (argv[++i] || '').trim();
    } else if (a === '--selected-resource-ids') {
      out.selectedResourceIds = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
    } else if (a === '--ext') {
      const raw = (argv[++i] || '').trim();
      try {
        out.ext = JSON.parse(raw);
      } catch {
        console.error('Error: --ext must be valid JSON');
        process.exit(1);
      }
    } else if (a === '--accept-language') {
      out.acceptLanguage = (argv[++i] || '').trim();
    } else if (a === '--timeout' || a === '-t') {
      const n = parseInt(argv[++i] || '', 10);
      if (Number.isFinite(n) && n > 0) out.timeoutSec = n;
    } else if (!a.startsWith('-') && !out.query) {
      out.query = a.trim();
    }
  }

  return out;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function getMessage(payload) {
  return payload?.message || payload?.error || payload?.msg || payload?.code || 'Unknown error';
}

function isApiError(payload) {
  const status = payload?.status;
  const code = payload?.code;
  if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
  if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
  return false;
}

async function fetchJson(url, init, timeoutMs) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, { ...init, signal: controller.signal });
    let body = {};
    try {
      body = await res.json();
    } catch {
      body = {};
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
    if (isApiError(body)) throw new Error(getMessage(body));
    return body;
  } catch (err) {
    if (err?.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

// ── API ──

async function createConversation(apiKey, apiBase, body, timeoutMs, threadId) {
  const url = threadId
    ? `${apiBase}/v2/conversations/${encodeURIComponent(threadId)}/follow_up`
    : `${apiBase}/v2/conversations`;
  const payload = await fetchJson(
    url,
    {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    },
    timeoutMs
  );
  const data = payload?.data ?? {};
  if (!data.stream_key) throw new Error('Unexpected response: missing stream_key');
  return data;
}

// ── SSE ──

function extractToolParams(data) {
  const out = [];
  const tools = data?.tools;
  if (!Array.isArray(tools)) return out;
  for (const t of tools) {
    if (HIDDEN_TOOLS.has(t?.name) || HIDDEN_TOOLS.has(t?.tool_name)) continue;
    if (t?.name && t?.params) out.push({ name: t.name, params: t.params });
  }
  return out;
}

function extractToolResults(data) {
  const out = [];
  const tools = data?.tools;
  if (!Array.isArray(tools)) return out;
  for (const t of tools) {
    if (HIDDEN_TOOLS.has(t?.name) || HIDDEN_TOOLS.has(t?.tool_name)) continue;
    const callResult = t?.call_result;
    if (t?.tool_name === 'generate_images' || t?.name === 'generate_images') {
      if (!callResult) continue;
      if (Array.isArray(callResult)) {
        for (const item of callResult) {
          if (item?.image_url) out.push({ type: 'image', title: item?.title || '', image_url: item.image_url, file_id: item?.file_id || null });
        }
      } else if (callResult?.images && Array.isArray(callResult.images)) {
        for (const img of callResult.images) {
          if (img?.image_url) out.push({ type: 'image', title: img?.title || '', image_url: img.image_url, file_id: img?.file_id || null });
        }
      } else if (callResult?.image_url) {
        out.push({ type: 'image', title: callResult?.title || '', image_url: callResult.image_url, file_id: callResult?.file_id || null });
      }
    }
    if (t?.name === 'generate_discovery' && callResult?.status === 'success') {
      out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || 'Discovery' });
    }
    if (t?.name === 'generate_document' && callResult?.status === 'success') {
      out.push({ type: 'document', title: callResult?.title || t?.params?.title || 'Document' });
    }
    if (t?.name === 'generate_ppt' && callResult?.status === 'success') {
      out.push({ type: 'ppt', title: callResult?.title || t?.params?.title || 'PPT' });
    }
    if (t?.name === 'generate_html' && callResult?.status === 'success') {
      out.push({ type: 'html', title: callResult?.title || t?.params?.title || 'HTML' });
    }
    if (t?.name === 'search_x' && callResult?.tweets && Array.isArray(callResult.tweets)) {
      out.push({ type: 'search_x', status: callResult.status, tweets: callResult.tweets });
    }
  }
  return out;
}

function dispatch(eventType, dataStr, callbacks) {
  const { onMessage, onToolCall, onToolResult } = callbacks;
  let payload = {};
  if (dataStr) {
    try {
      payload = JSON.parse(dataStr);
    } catch {
      payload = { content: dataStr };
    }
  }

  switch (eventType) {
    case 'message':
      if (typeof payload.content === 'string') onMessage(payload.content);
      break;
    case 'stream': {
      const content = payload?.content;
      if (typeof content === 'string') {
        try {
          const inner = JSON.parse(content);
          const type = inner?.type;
          const data = inner?.data;
          if (type === 'content' || type === 'text' || type === 'delta' || type === 'answer') {
            const text = data?.content ?? data?.text ?? data?.delta;
            if (typeof text === 'string') onMessage(text);
          } else if (type === 'tools' && onToolCall) {
            const params = extractToolParams(data);
            for (const item of params) onToolCall(item);
          } else if ((type === 'tools_result_stream' || type === 'tools_result') && onToolResult) {
            const results = extractToolResults(data);
            for (const item of results) onToolResult(item);
          } else if (type !== 'processing' && type !== 'tools' && type !== 'message' && data?.message && typeof data.message === 'string') {
            onMessage(data.message);
          }
        } catch {
          onMessage(content);
        }
      }
      break;
    }
    case 'heartbeat':
    case 'connected':
      break;
    default:
      break;
  }
}

async function readSSE(url, apiKey, startOffset, callbacks) {
  const { onMessage, onDone, onEvent, onToolCall, onToolResult } = callbacks;
  const controller = new AbortController();
  let idleTimer = null;
  const resetIdleTimer = () => {
    if (idleTimer) clearTimeout(idleTimer);
    idleTimer = setTimeout(() => controller.abort(), STREAM_IDLE_TIMEOUT_MS);
  };

  const connectUrl = startOffset >= 0 ? `${url}?offset=${startOffset}` : url;
  let maxOffset = startOffset;
  let streamDone = false;

  try {
    const res = await fetch(connectUrl, {
      method: 'GET',
      headers: {
        Accept: 'text/event-stream',
        Authorization: `Bearer ${apiKey}`,
      },
      signal: controller.signal,
    });

    if (!res.ok) {
      const text = await res.text();
      let msg = text;
      try {
        const j = JSON.parse(text);
        msg = getMessage(j) || text;
      } catch {}
      throw new Error(`HTTP ${res.status}: ${msg}`);
    }

    const reader = res.body?.getReader();
    if (!reader) throw new Error('No response body');

    const decoder = new TextDecoder();
    let buffer = '';
    let currentEvent = '';
    let currentData = undefined;

    const processEvent = (evt, data) => {
      if (!evt || data === undefined) return;
      let eventOffset = -1;
      try {
        const parsed = JSON.parse(data);
        if (typeof parsed?.offset === 'number') {
          eventOffset = parsed.offset;
          if (parsed.offset > maxOffset) maxOffset = parsed.offset;
        }
      } catch {}

      if (eventOffset >= 0 && eventOffset <= startOffset) return;

      if (onEvent) onEvent(evt, data);

      if (evt === 'error') return; // server "not ready yet" signal, keep reading
      if (evt === 'done' || evt === 'completed' || evt === 'complete') {
        streamDone = true;
        onDone();
        return;
      }
      dispatch(evt, data, { onMessage, onToolCall, onToolResult });
    };

    resetIdleTimer();
    while (true) {
      const { done, value } = await reader.read();
      resetIdleTimer();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split(/\r?\n/);
      buffer = lines.pop() ?? '';

      for (const line of lines) {
        if (line.startsWith('event:')) {
          processEvent(currentEvent, currentData);
          currentEvent = line.slice(6).trim();
          currentData = undefined;
        } else if (line.startsWith('data:')) {
          currentData = line.slice(5).trim();
        } else if (line === '') {
          processEvent(currentEvent, currentData);
          currentEvent = '';
          currentData = undefined;
        }
      }
    }

    if (idleTimer) clearTimeout(idleTimer);
    processEvent(currentEvent, currentData);
  } catch (err) {
    if (idleTimer) clearTimeout(idleTimer);
    if (err?.name === 'AbortError') {
      return { maxOffset, streamDone, streamError: `Stream idle timeout (no data for ${STREAM_IDLE_TIMEOUT_MS / 1000}s)` };
    }
    throw err;
  }

  return { maxOffset, streamDone, streamError: null };
}

async function consumeStream(apiKey, apiBase, streamKey, callbacks) {
  const url = `${apiBase}/v2/conversations/stream/${encodeURIComponent(streamKey)}`;
  let lastOffset = -1;
  const startTime = Date.now();

  while (true) {
    if (Date.now() - startTime > STREAM_IDLE_TIMEOUT_MS) {
      callbacks.onError('Stream timeout: no completion after ' + (STREAM_IDLE_TIMEOUT_MS / 1000) + 's');
      return;
    }

    const result = await readSSE(url, apiKey, lastOffset, callbacks);

    if (result.streamDone) return;
    if (result.maxOffset > lastOffset) lastOffset = result.maxOffset;

    await sleep(RECONNECT_DELAY_MS);
  }
}

// ── Format helpers ──

function formatTweet(tweet) {
  const info = tweet?.other_info || {};
  const author = info.author || {};
  const metrics = info.metrics || {};
  const name = author.display_name || author.username || 'Unknown';
  const handle = author.username ? `@${author.username}` : '';
  const text = tweet?.snippet || tweet?.title || '';
  const link = tweet?.link || info.url || '';
  const stats = [];
  if (metrics.favorite_count) stats.push(`${metrics.favorite_count} likes`);
  if (metrics.retweet_count) stats.push(`${metrics.retweet_count} retweets`);
  if (metrics.view_count) stats.push(`${metrics.view_count} views`);
  const statsStr = stats.length > 0 ? `  [${stats.join(' | ')}]` : '';
  return `  ${name} (${handle})${statsStr}\n  ${text}\n  ${link}`;
}

// ── Main ──

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    usage();
    process.exit(0);
  }
  if (!args.query) {
    usage();
    process.exit(1);
  }

  const [REDACTED_SECRET]);
  if (!apiKey) {
    console.error(
      'ERROR: FELO_API_KEY not set\n\n' +
      'To use SuperAgent, set FELO_API_KEY:\n' +
      '  export FELO_API_KEY="your-api-key-here"\n' +
      'Get your API key from https://felo.ai (Settings -> API Keys).'
    );
    process.exit(1);
  }

  const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
  const timeoutMs = args.timeoutSec * 1000;

  // Build request body
  const body = { query: args.query.slice(0, 2000) };
  if (args.liveDocId) body.live_doc_short_id = args.liveDocId;
  if (args.acceptLanguage) body.accept_language = args.acceptLanguage;

  const threadId = args.threadId || undefined;

  // skill_id, selected_resource_ids, ext only for new conversations
  if (threadId && (args.skillId || args.selectedResourceIds.length || args.ext)) {
    process.stderr.write('Warning: --skill-id, --selected-resource-ids, --ext are ignored in follow-up mode.\n');
  }
  if (!threadId) {
    if (args.skillId) body.skill_id = args.skillId;
    if (args.selectedResourceIds.length) body.selected_resource_ids = args.selectedResourceIds;
    if (args.ext) body.ext = args.ext;
  }

  process.stderr.write(threadId ? 'SuperAgent: following up...\n' : 'SuperAgent: creating conversation...\n');

  const createData = await createConversation(apiKey, apiBase, body, timeoutMs, threadId);
  const { stream_key, thread_short_id, live_doc_short_id } = createData;

  if (args.verbose) {
    process.stderr.write(`Stream key: ${stream_key}\n`);
    process.stderr.write(`Thread ID: ${thread_short_id}\n`);
    process.stderr.write(`LiveDoc ID: ${live_doc_short_id}\n`);
  }

  const feloBase = (process.env.FELO_WEB_BASE?.trim() || apiBase.replace(/\/\/openapi-/, '//').replace(/\/\/openapi\./, '//')).replace(/\/$/, '');
  const liveDocUrl = live_doc_short_id ? `${feloBase}/zh-Hans/livedoc/${live_doc_short_id}` : '';

  const chunks = [];
  const toolResults = [];
  const seenKeys = new Set();
  const isJson = args.json;

  const onToolCall = (item) => {
    if (isJson) return;
    const { name, params } = item;
    console.log(`\n[Tool: ${name}]`);
    if (name === 'search_x') {
      console.log(`  Query: ${params.query || ''}`);
      if (params.query_type) console.log(`  Type: ${params.query_type}`);
      if (params.limit) console.log(`  Limit: ${params.limit}`);
    } else if (name === 'generate_images') {
      const images = params?.images;
      if (Array.isArray(images)) {
        for (const img of images) console.log(`  Image: ${img.title || '(untitled)'}`);
      }
    } else if (name === 'generate_discovery') {
      console.log(`  Title: ${params.title || params.query || ''}`);
    } else if (name === 'generate_document') {
      console.log(`  Title: ${params.title || ''}`);
    } else if (name === 'generate_ppt') {
      console.log(`  Title: ${params.title || ''}`);
    } else if (name === 'generate_html') {
      console.log(`  Title: ${params.title || ''}`);
    } else {
      console.log(`  Params: ${JSON.stringify(params)}`);
    }
  };

  const onToolResult = (item) => {
    const LIVEDOC_TYPES = new Set(['document', 'ppt', 'html', 'discovery']);
    const key = (item?.type === 'image' && item?.file_id)
      ? `file:${item.file_id}`
      : item?.image_url || (LIVEDOC_TYPES.has(item?.type) ? item.type : `${item?.type}:${item?.title}`);
    if (seenKeys.has(key)) return;
    seenKeys.add(key);
    toolResults.push(item);

    if (isJson) return;
    if (item.type === 'image') {
      console.log(liveDocUrl ? `[${item.title || 'Image'}](${liveDocUrl})` : item.image_url);
    } else if (item.type === 'discovery') {
      console.log(liveDocUrl ? `[${item.title}](${liveDocUrl})` : item.title);
    } else if (item.type === 'document') {
      console.log(liveDocUrl ? `[${item.title || 'Document'}](${liveDocUrl})` : (item.title || 'Document'));
    } else if (item.type === 'ppt') {
      console.log(liveDocUrl ? `[${item.title || 'PPT'}](${liveDocUrl})` : (item.title || 'PPT'));
    } else if (item.type === 'html') {
      console.log(liveDocUrl ? `[${item.title || 'HTML'}](${liveDocUrl})` : (item.title || 'HTML'));
    } else if (item.type === 'search_x') {
      console.log(`\n[Twitter Search Results] (${item.tweets.length} tweets)`);
      for (const tweet of item.tweets) {
        console.log(formatTweet(tweet));
        console.log('');
      }
    }
  };

  let streamError = null;
  const onEvent = args.verbose
    ? (eventType, dataStr) => {
        process.stderr.write(`[stream] event=${eventType}\n`);
        process.stderr.write(`[stream] data=${dataStr || ''}\n`);
      }
    : undefined;

  await consumeStream(apiKey, apiBase, stream_key, {
    onMessage: (content) => {
      chunks.push(content);
      if (!isJson) process.stdout.write(content);
    },
    onError: (err) => {
      streamError = err;
    },
    onDone: () => {},
    onEvent,
    onToolCall,
    onToolResult,
  });

  if (streamError) throw new Error(streamError);

  const answer = chunks.join('').trim();

  if (isJson) {
    const images = toolResults.filter((r) => r.type === 'image');
    const discoveries = toolResults.filter((r) => r.type === 'discovery');
    const documents = toolResults.filter((r) => r.type === 'document');
    const ppts = toolResults.filter((r) => r.type === 'ppt');
    const htmls = toolResults.filter((r) => r.type === 'html');
    const searches = toolResults.filter((r) => r.type === 'search_x');
    console.log(
      JSON.stringify(
        {
          status: 'ok',
          data: {
            answer: answer || null,
            thread_short_id: thread_short_id ?? null,
            live_doc_short_id: live_doc_short_id ?? null,
            image_urls: images.length > 0 ? images.map((r) => ({ url: r.image_url, title: r.title, ...(r.file_id ? { file_id: r.file_id } : {}) })) : undefined,
            discoveries: discoveries.length > 0 ? discoveries.map((r) => ({ title: r.title })) : undefined,
            documents: documents.length > 0 ? documents.map((r) => ({ title: r.title })) : undefined,
            ppts: ppts.length > 0 ? ppts.map((r) => ({ title: r.title })) : undefined,
            htmls: htmls.length > 0 ? htmls.map((r) => ({ title: r.title })) : undefined,
            search_x: searches.length > 0 ? searches.map((r) => ({ tweets: r.tweets })) : undefined,
            live_doc_url: liveDocUrl || undefined,
          },
        },
        null,
        2
      )
    );
  } else {
    if (answer) console.log('');
    if (!answer && toolResults.length === 0) console.log('(No content in stream)');
    process.stderr.write(`\n[state] thread_short_id=${thread_short_id || ''} live_doc_short_id=${live_doc_short_id || ''} live_doc_url=${liveDocUrl || ''}\n`);
  }
}

main().catch((err) => {
  console.error(`ERROR: ${err?.message || err}`);
  process.exit(1);
});


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