
Felo Superagent
- 80 installs
- 241 repo stars
- Updated July 22, 2026
- felo-inc/felo-skills
Helps with ai & agent building tasks.
About
felo-superagent is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- felo-superagent
- AI & Agent Building
- AI-coding skill
Felo Superagent by the numbers
- 80 all-time installs (skills.sh)
- Ranked #5,257 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/felo-inc/felo-skills --skill felo-superagentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 80 |
|---|---|
| repo stars | ★ 241 |
| Last updated | July 22, 2026 |
| Repository | felo-inc/felo-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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-idonly 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 postslogo-and-branding— For creating logos, brand designs, or visual identityecommerce-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-idwith that value - If none of the above match, do NOT pass
--skill-id(general conversation mode) --skill-idis 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.mjsfor 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 DNAorCover file IDdepending on what is present - Use the category that matches the skill:
TWITTERfortwitter-writer,IMAGEforlogo-and-brandingandecommerce-product-image. - Always pass
--accept-languagetorun_style_library.mjsso 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. --extis 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-brandingskill) - E-commerce images: Generate product images (auto-selects
ecommerce-product-imageskill) - 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-writerinstead) - 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 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:
export FELO_API_KEY="your-api-key-here"For permanent configuration, add to your shell profile (~/.bashrc or ~/.zshrc):
echo 'export FELO_API_KEY="your-api-key-here"' >> ~/.zshrc
source ~/.zshrcWindows (PowerShell):
$env:FELO_API_KEY="your-api-key-here"Windows (CMD):
set FELO_API_KEY=your-api-key-here3. 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
if [ -z "$FELO_API_KEY" ]; then
echo "ERROR: FELO_API_KEY not set"
exit 1
fiIf 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:
node felo-livedoc/scripts/run_livedoc.mjs list --jsonParse 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:
{
"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:
node felo-livedoc/scripts/run_livedoc.mjs create --name "SuperAgent Workspace" --jsonParse the JSON output and extract data.short_id as your live_doc_id.
Example response:
{
"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 postslogo-and-branding— For creating logos, brand designs, or visual identityecommerce-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.
# 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 styleIf 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:
--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):
node felo-superAgent/scripts/run_superagent.mjs \
--query "USER_QUERY_HERE" \
--live-doc-id "LIVE_DOC_ID" \
--accept-language en \
--jsonNew conversation with skill ID, no style selected:
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 \
--jsonNew conversation with skill ID and brand style (from Step 4.5):
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 \
--jsonFollow-up question (DEFAULT for 2nd+ messages):
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" \
--jsonStep 6: Extract State and Output the Answer
After the script finishes, parse the JSON output:
{
"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": "b9e5be11-7686-4aa8-ae6c-9876511a7b5c"
}
]
}
}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:
node felo-superAgent/scripts/run_superagent.mjs \
--query "What is quantum computing?" \
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
--accept-language en \
--jsonStep 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:
node felo-superAgent/scripts/run_superagent.mjs \
--query "What are its practical applications?" \
--thread-id "CmYpuGwBgCnrUdDx5ZtmxA" \
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
--jsonStep 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):
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language enOutput:
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:
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 \
--jsonStep 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:
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" \
--jsonExample 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:
node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language enOutput example:
Style name: Minimalist Modern
Style labels: clean, monochrome
Style DNA: ...(full content)
Cover file ID: file_333Present styles to user and wait for selection. Suppose user picks "Minimalist Modern":
Step 5:
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 \
--jsonExample 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:
node felo-superAgent/scripts/run_style_library.mjs --category IMAGE --accept-language enPresent styles to user. Suppose user picks "No preference":
Step 5: (no --ext since user chose no preference)
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 \
--jsonExample E: User Requests a New Canvas
User: "Open a new canvas for a different project"Step 2d: Create new LiveDoc:
node felo-livedoc/scripts/run_livedoc.mjs create --name "New Project" --jsonExtract 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:
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language enFind the entry with Style name: darioamodei, extract its full block verbatim. No need to ask the user again.
Step 5:
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 \
--jsonAvailable 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(requireslive_doc_short_idin 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 contentstream— Wrapped content with type informationheartbeat— Keep-alive signaldone/completed/complete— Stream finishederror— 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: errorwithdata: {"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 environmentTimeout 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_keyhas a limited validity period - Use the bundled Node script to consume SSE; do not assume
jqor 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_idis 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.
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language enOptions:
--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 (namefield)Style labels— fromcontent.labels(TWITTER) orcontent.tags(other categories), in the requested language, comma-separated; omitted if not presentStyle DNA— fromcontent.styleDna(TWITTER type); omitted if not presentCover file ID— fromcoverFileId; omitted if null/empty
User-created styles appear before recommended styles.
References
- SuperAgent API (Felo Open Platform)
- Felo Open Platform
- Get API Key (Settings -> API Keys)
{
"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.2",
"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"]
}
MIT License
Copyright (c) 2026 Felo SuperAgent Skill Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
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
--extto guide output (used byfelo-twitter-writerand 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:
clawhub install felo-superAgentManual install:
# 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 and log in 2. Avatar → Settings → API Keys → Create and copy key 3. Set environment variable:
# 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 moreRun script directly:
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`):
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
node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language enOutput (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_abc123Fields 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`:
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 enCategory 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
# 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 shopWhat 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-idfor 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_idandlive_doc_short_idfrom 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):
{
"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
node felo-superAgent/scripts/run_superagent.mjs \
--query "What is quantum computing?" \
--accept-language enExample 2: Follow-up in thread
# 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
# 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 enExample 4: Logo design with brand style
# 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 |
SUPER_AGENT_CONVERSATION_CREATE_FAILED (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
export FELO_API_BASE="https://custom-api.example.com"
node felo-superAgent/scripts/run_superagent.mjs --query "test"Resource selection
node felo-superAgent/scripts/run_superagent.mjs \
--query "Summarize these documents" \
--live-doc-id "PvyKouzJirXjFdst4uKRK3" \
--selected-resource-ids "res1,res2,res3"---
References
- SKILL.md — full agent instructions, decision logic, and style library format
- SuperAgent API Documentation
- Felo Open Platform
- Get API Key (Settings → API Keys)
- GitHub Repository
#!/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 apiKey = process.env.FELO_API_KEY?.trim();
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();
#!/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 apiKey = process.env.FELO_API_KEY?.trim();
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(`[${item.title || 'Image'}](${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);
// Replace internal preview URLs in answer text with signed S3 URLs from tool results
let answer = chunks.join('').trim();
for (const r of toolResults) {
if (r.type === 'image' && r.file_id && r.image_url) {
const previewUrl = `https://api.felo.ai/search/files/${r.file_id}/preview`;
answer = answer.split(previewUrl).join(r.image_url);
}
}
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 && toolResults.length === 0) console.log('(No content in stream)');
const images = toolResults.filter((r) => r.type === 'image');
if (images.length > 0 || liveDocUrl) {
console.log('\n---');
for (const r of images) {
console.log(`[${r.title || 'Image'}](${r.image_url})`);
}
if (liveDocUrl) console.log(`LiveDoc: ${liveDocUrl}`);
}
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);
});