diff --git a/skills/aisa/SKILL.md b/skills/aisa/SKILL.md new file mode 100644 index 000000000..a604e96f5 --- /dev/null +++ b/skills/aisa/SKILL.md @@ -0,0 +1,210 @@ +--- +name: openclaw-starter-kit +description: "OpenClaw Starter Kit - The definitive starting point for building autonomous agents. Powered by AIsa unified API: Twitter/X (read & write), web search, academic papers, news, and LLM routing with one API key." +homepage: https://openclaw.ai +metadata: {"openclaw":{"emoji":"🦞","requires":{"bins":["curl","python3"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}} +--- + +# OpenClaw Starter Kit 🦞 + +**The definitive starting point for autonomous agents. Powered by AIsa.** + +One API key. All the data sources your agent needs. + +## 🔥 What Can You Do? + +### Morning Briefing (Scheduled) +``` +"Send me a daily briefing at 8am with: +- My portfolio performance (NVDA, TSLA, BTC) +- Twitter trends in AI +- Top news in my industry" +``` + +### Competitor Intelligence +``` +"Monitor @OpenAI - alert me on new tweets, news mentions, and paper releases" +``` + +### Investment Research +``` +"Full analysis on NVDA: price trends, insider trades, analyst estimates, +SEC filings, and Twitter sentiment" +``` + +### Startup Validation +``` +"Research the market for AI writing tools - find competitors, +Twitter discussions, and academic papers on the topic" +``` + +### Crypto Whale Alerts +``` +"Track large BTC movements and correlate with Twitter activity" +``` + +## AIsa vs bird + +| Feature | AIsa ⚡ | bird 🐦 | +|---------|---------|---------| +| Auth method | API Key (simple) | Browser cookies (complex) | +| Read Twitter | ✅ | ✅ | +| Post/Like/Retweet | ✅ (via login) | ✅ | +| Web Search | ✅ | ❌ | +| Scholar Search | ✅ | ❌ | +| News/Financial | ✅ | ❌ | +| LLM Routing | ✅ | ❌ | +| Server-friendly | ✅ | ❌ | +| Cost | Pay-per-use | Free | + +**Use AIsa when**: Server environment, need search/scholar APIs, prefer simple API key setup. +**Use bird when**: Local machine with browser, need free access, complex Twitter interactions. + +## Quick Start + +```bash +export AISA_API_KEY="your-key" +``` + +## Core Capabilities + +### Twitter/X Data (Read) + +```bash +# Get user info +curl "https://api.aisa.one/apis/v1/twitter/user/info?userName=elonmusk" \ + -H "Authorization: Bearer $AISA_API_KEY" + +# Advanced tweet search +curl "https://api.aisa.one/apis/v1/twitter/tweet/advanced_search?query=AI+agents&queryType=Latest" \ + -H "Authorization: Bearer $AISA_API_KEY" + +# Get trending topics (worldwide) +curl "https://api.aisa.one/apis/v1/twitter/trends?woeid=1" \ + -H "Authorization: Bearer $AISA_API_KEY" +``` + +### Twitter/X Post (Write) + +> ⚠️ **Warning**: Posting requires account login. Use responsibly to avoid rate limits or account suspension. + +```bash +# Step 1: Login first (async, check status after) +curl -X POST "https://api.aisa.one/apis/v1/twitter/user_login_v3" \ + -H "Authorization: Bearer $AISA_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_name":"myaccount","email":"me@example.com","password":"xxx","proxy":"http://user:pass@ip:port"}' + +# Step 2: Send tweet +curl -X POST "https://api.aisa.one/apis/v1/twitter/send_tweet_v3" \ + -H "Authorization: Bearer $AISA_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_name":"myaccount","text":"Hello from OpenClaw!"}' + +# Like / Retweet +curl -X POST "https://api.aisa.one/apis/v1/twitter/like_tweet_v3" \ + -H "Authorization: Bearer $AISA_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_name":"myaccount","tweet_id":"1234567890"}' +``` + +### Search (Web + Academic) + +```bash +# Web search +curl -X POST "https://api.aisa.one/apis/v1/scholar/search/web?query=AI+frameworks&max_num_results=10" \ + -H "Authorization: Bearer $AISA_API_KEY" + +# Academic/scholar search +curl -X POST "https://api.aisa.one/apis/v1/scholar/search/scholar?query=transformer+models&max_num_results=10" \ + -H "Authorization: Bearer $AISA_API_KEY" + +# Smart search (web + academic combined) +curl -X POST "https://api.aisa.one/apis/v1/scholar/search/smart?query=machine+learning&max_num_results=10" \ + -H "Authorization: Bearer $AISA_API_KEY" +``` + +### Financial News + +```bash +# Company news by ticker +curl "https://api.aisa.one/apis/v1/financial/news?ticker=AAPL&limit=10" \ + -H "Authorization: Bearer $AISA_API_KEY" +``` + +### LLM Routing (OpenAI Compatible) + +```bash +curl -X POST "https://api.aisa.one/v1/chat/completions" \ + -H "Authorization: Bearer $AISA_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}' +``` + +Supported models: GPT-4, Claude-3, Gemini, Qwen, Deepseek, Grok, and more. + +## Python Client + +```bash +# Twitter Read +python3 {baseDir}/scripts/aisa_client.py twitter user-info --username elonmusk +python3 {baseDir}/scripts/aisa_client.py twitter search --query "AI agents" +python3 {baseDir}/scripts/aisa_client.py twitter trends --woeid 1 + +# Twitter Write (requires login first) +python3 {baseDir}/scripts/aisa_client.py twitter login --username myaccount --email me@example.com --password xxx --proxy "http://user:pass@ip:port" +python3 {baseDir}/scripts/aisa_client.py twitter post --username myaccount --text "Hello!" +python3 {baseDir}/scripts/aisa_client.py twitter like --username myaccount --tweet-id 1234567890 + +# Search +python3 {baseDir}/scripts/aisa_client.py search web --query "latest AI news" +python3 {baseDir}/scripts/aisa_client.py search scholar --query "LLM research" +python3 {baseDir}/scripts/aisa_client.py search smart --query "machine learning" + +# News +python3 {baseDir}/scripts/aisa_client.py news --ticker AAPL + +# LLM +python3 {baseDir}/scripts/aisa_client.py llm complete --model gpt-4 --prompt "Explain quantum computing" +``` + +## Pricing + +| API | Cost | +|-----|------| +| Twitter query | ~$0.0004 | +| Twitter post/like | ~$0.001 | +| Web search | ~$0.001 | +| Scholar search | ~$0.002 | +| News | ~$0.001 | +| LLM | Token-based | + +Every response includes `usage.cost` and `usage.credits_remaining`. + +## Error Handling + +Errors return JSON with `error` field: + +```json +{ + "error": "Invalid API key", + "code": 401 +} +``` + +Common error codes: +- `401` - Invalid or missing API key +- `402` - Insufficient credits +- `429` - Rate limit exceeded +- `500` - Server error + +## Get Started + +1. Sign up at [aisa.one](https://aisa.one) +2. Get your API key +3. Add credits (pay-as-you-go) +4. Set environment variable: `export AISA_API_KEY="your-key"` + +## Full API Reference + +See [references/api-reference.md](references/api-reference.md) for complete endpoint documentation. diff --git a/skills/aisa/references/api-reference.md b/skills/aisa/references/api-reference.md new file mode 100644 index 000000000..4804917c2 --- /dev/null +++ b/skills/aisa/references/api-reference.md @@ -0,0 +1,198 @@ +# OpenClaw Starter Kit - API Reference + +**Powered by AIsa** + +Complete API documentation based on [aisa.mintlify.app](https://aisa.mintlify.app/api-reference/introduction). + +## Base URL + +``` +https://api.aisa.one/apis/v1 +``` + +## Authentication + +All requests require a Bearer token: + +``` +Authorization: Bearer YOUR_AISA_API_KEY +``` + +--- + +## Twitter/X APIs + +### GET /twitter/user/info + +Get user information by username. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| userName | string | Yes | Twitter username (without @) | + +### GET /twitter/tweet/advanced_search + +Advanced search for tweets. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| query | string | Yes | Search query | +| queryType | string | Yes | "Latest" or "Top" | +| cursor | string | No | Pagination cursor | + +### GET /twitter/user/user_last_tweet + +Get user's recent tweets. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| userName | string | Yes | Twitter username | + +### GET /twitter/tweet/tweetById + +Get tweets by IDs. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| tweet_ids | string | Yes | Comma-separated tweet IDs | + +### GET /twitter/trends + +Get trending topics by WOEID. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| woeid | integer | Yes | WOEID (1 = worldwide) | +| count | integer | No | Number of trends (default 30) | + +### GET /twitter/user/search_user + +Search for users by keyword. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| keyword | string | Yes | Search keyword | + +--- + +## Search APIs + +### POST /scholar/search/web + +Web search with structured results. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| query | string | Yes | Search query | +| max_num_results | integer | No | Max results (1-100, default 10) | +| as_ylo | integer | No | Year lower bound | +| as_yhi | integer | No | Year upper bound | + +### POST /scholar/search/scholar + +Academic paper search. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| query | string | Yes | Search query | +| max_num_results | integer | No | Max results (1-100, default 10) | +| as_ylo | integer | No | Year lower bound | +| as_yhi | integer | No | Year upper bound | + +### POST /scholar/search/smart + +Intelligent search combining web and academic results. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| query | string | Yes | Search query | +| max_num_results | integer | No | Max results | + +--- + +## Tavily APIs + +### POST /tavily/search + +Tavily search integration. + +### POST /tavily/extract + +Extract content from URLs. + +### POST /tavily/crawl + +Crawl web pages. + +--- + +## Financial APIs + +### GET /financial/news/company + +Company news by ticker. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| ticker | string | Yes | Stock ticker (e.g., AAPL) | +| limit | integer | No | Number of articles | + +### Other Financial Endpoints + +- `/financial/stock/prices` - Historical stock prices +- `/financial/financial_statements/*` - Income, balance, cash flow +- `/financial/company/facts` - Company facts by CIK +- `/financial/search/stock` - Stock screener + +--- + +## LLM APIs (OpenAI Compatible) + +Base URL for LLM: `https://api.aisa.one/v1` + +### POST /v1/chat/completions + +OpenAI-compatible chat completions. + +```json +{ + "model": "gpt-4", + "messages": [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello!"} + ], + "max_tokens": 1000, + "temperature": 0.7 +} +``` + +**Supported Models:** + +| Provider | Models | +|----------|--------| +| OpenAI | gpt-4, gpt-4-turbo, gpt-3.5-turbo | +| Anthropic | claude-3-opus, claude-3-sonnet, claude-3-haiku | +| Google | gemini-pro, gemini-ultra | +| Alibaba | qwen-* | +| Deepseek | deepseek-* | +| xAI | grok-* | + +--- + +## Error Handling + +```json +{ + "error": "error message", + "code": 400, + "details": "additional info" +} +``` + +--- + +## Full Documentation + +For complete API documentation including all endpoints: +- [AIsa API Reference](https://aisa.mintlify.app/api-reference/introduction) +- [Documentation Index](https://aisa.mintlify.app/llms.txt) diff --git a/skills/aisa/scripts/aisa_client.py b/skills/aisa/scripts/aisa_client.py new file mode 100644 index 000000000..68d98f867 --- /dev/null +++ b/skills/aisa/scripts/aisa_client.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python3 +""" +OpenClaw Starter Kit - AIsa API Client +Powered by AIsa (https://aisa.one) + +Unified API access for autonomous agents. + +Usage: + python aisa_client.py twitter user-info --username + python aisa_client.py twitter tweets --username [--count ] + python aisa_client.py twitter search --query [--count ] + python aisa_client.py twitter detail --tweet-id + python aisa_client.py twitter trends + python aisa_client.py search web --query [--count ] + python aisa_client.py search scholar --query [--count ] + python aisa_client.py news --query [--count ] + python aisa_client.py llm complete --model --prompt + python aisa_client.py llm chat --model --messages +""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.parse +import urllib.error +from typing import Any, Dict, Optional + + +class AIsaClient: + """OpenClaw Starter Kit - AIsa API Client for unified access to AI-native data sources.""" + + BASE_URL = "https://api.aisa.one/apis/v1" + LLM_BASE_URL = "https://api.aisa.one/v1" + + def __init__(self, api_key: Optional[str] = None): + """Initialize the client with an API key.""" + self.api_key = api_key or os.environ.get("AISA_API_KEY") + if not self.api_key: + raise ValueError( + "AISA_API_KEY is required. Set it via environment variable or pass to constructor." + ) + + def _request( + self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Make an HTTP request to the AIsa API.""" + url = f"{self.BASE_URL}{endpoint}" + + if params: + query_string = urllib.parse.urlencode( + {k: v for k, v in params.items() if v is not None} + ) + url = f"{url}?{query_string}" + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "User-Agent": "OpenClaw-Starter-Kit/1.0" + } + + request_data = None + if data: + request_data = json.dumps(data).encode("utf-8") + + # For POST requests without body, send empty JSON + if method == "POST" and request_data is None: + request_data = b"{}" + + req = urllib.request.Request(url, data=request_data, headers=headers, method=method) + + try: + with urllib.request.urlopen(req, timeout=60) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8") + try: + return json.loads(error_body) + except json.JSONDecodeError: + return {"success": False, "error": {"code": str(e.code), "message": error_body}} + except urllib.error.URLError as e: + return {"success": False, "error": {"code": "NETWORK_ERROR", "message": str(e.reason)}} + + # ==================== Twitter APIs ==================== + + def twitter_user_info(self, username: str) -> Dict[str, Any]: + """Get Twitter user information by username.""" + return self._request("GET", "/twitter/user/info", params={"userName": username}) + + def twitter_user_tweets(self, username: str, count: int = 20) -> Dict[str, Any]: + """Get tweets from a specific user.""" + return self._request("GET", "/twitter/user/user_last_tweet", params={"userName": username}) + + def twitter_search(self, query: str, query_type: str = "Latest") -> Dict[str, Any]: + """Search for tweets matching a query (Advanced Search).""" + return self._request("GET", "/twitter/tweet/advanced_search", params={ + "query": query, + "queryType": query_type # "Latest" or "Top" + }) + + def twitter_tweet_detail(self, tweet_ids: str) -> Dict[str, Any]: + """Get detailed information about tweets by IDs (comma-separated).""" + return self._request("GET", "/twitter/tweet/tweetById", params={"tweet_ids": tweet_ids}) + + def twitter_trends(self, woeid: int = 1) -> Dict[str, Any]: + """Get current Twitter trending topics by WOEID (1 = worldwide).""" + return self._request("GET", "/twitter/trends", params={"woeid": woeid}) + + def twitter_user_search(self, keyword: str) -> Dict[str, Any]: + """Search for Twitter users by keyword.""" + return self._request("GET", "/twitter/user/search_user", params={"keyword": keyword}) + + # ==================== Twitter Post APIs (V3 - requires login) ==================== + + def twitter_login(self, username: str, email: str, password: str, proxy: str, totp_code: str = None) -> Dict[str, Any]: + """Login to Twitter account (V3). Required before posting.""" + data = { + "user_name": username, + "email": email, + "password": password, + "proxy": proxy + } + if totp_code: + data["totp_code"] = totp_code + return self._request("POST", "/twitter/user_login_v3", data=data) + + def twitter_get_account(self, username: str) -> Dict[str, Any]: + """Get logged-in account details (check login status).""" + return self._request("GET", "/twitter/get_my_x_account_detail_v3", params={"user_name": username}) + + def twitter_send_tweet(self, username: str, text: str, media_base64: str = None, media_type: str = None, community_id: str = None) -> Dict[str, Any]: + """Send a tweet (requires prior login via twitter_login).""" + data = { + "user_name": username, + "text": text + } + if media_base64: + data["media_data_base64"] = media_base64 + if media_type: + data["media_type"] = media_type + if community_id: + data["community_id"] = community_id + return self._request("POST", "/twitter/send_tweet_v3", data=data) + + def twitter_like(self, username: str, tweet_id: str) -> Dict[str, Any]: + """Like a tweet (requires prior login).""" + return self._request("POST", "/twitter/like_tweet_v3", data={ + "user_name": username, + "tweet_id": tweet_id + }) + + def twitter_retweet(self, username: str, tweet_id: str) -> Dict[str, Any]: + """Retweet a tweet (requires prior login).""" + return self._request("POST", "/twitter/retweet_v3", data={ + "user_name": username, + "tweet_id": tweet_id + }) + + # ==================== Search APIs ==================== + + def search_web(self, query: str, max_results: int = 10) -> Dict[str, Any]: + """Perform a web search (POST method).""" + return self._request("POST", "/scholar/search/web", params={ + "query": query, + "max_num_results": max_results + }) + + def search_scholar(self, query: str, max_results: int = 10, year_from: int = None, year_to: int = None) -> Dict[str, Any]: + """Search academic papers and scholarly content (POST method).""" + params = { + "query": query, + "max_num_results": max_results + } + if year_from: + params["as_ylo"] = year_from + if year_to: + params["as_yhi"] = year_to + return self._request("POST", "/scholar/search/scholar", params=params) + + def search_smart(self, query: str, max_results: int = 10) -> Dict[str, Any]: + """Perform intelligent search combining web and academic results.""" + return self._request("POST", "/scholar/search/smart", params={ + "query": query, + "max_num_results": max_results + }) + + # ==================== News API ==================== + + def news(self, ticker: str, count: int = 10) -> Dict[str, Any]: + """Get company news by stock ticker.""" + return self._request("GET", "/financial/news", params={"ticker": ticker, "limit": count}) + + # ==================== LLM APIs ==================== + + def _llm_request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: + """Make an HTTP request to the AIsa LLM API (different base URL).""" + url = f"{self.LLM_BASE_URL}{endpoint}" + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "User-Agent": "OpenClaw-Starter-Kit/1.0" + } + + request_data = json.dumps(data).encode("utf-8") + req = urllib.request.Request(url, data=request_data, headers=headers, method="POST") + + try: + with urllib.request.urlopen(req, timeout=120) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8") + try: + return json.loads(error_body) + except json.JSONDecodeError: + return {"success": False, "error": {"code": str(e.code), "message": error_body}} + except urllib.error.URLError as e: + return {"success": False, "error": {"code": "NETWORK_ERROR", "message": str(e.reason)}} + + def llm_complete(self, model: str, prompt: str, **kwargs) -> Dict[str, Any]: + """Generate a completion using the specified LLM model.""" + data = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + **kwargs + } + return self._llm_request("/chat/completions", data) + + def llm_chat(self, model: str, messages: list, **kwargs) -> Dict[str, Any]: + """Perform a chat completion with message history.""" + data = { + "model": model, + "messages": messages, + **kwargs + } + return self._llm_request("/chat/completions", data) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="OpenClaw Starter Kit - Unified API access for autonomous agents (Powered by AIsa)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s twitter user-info --username elonmusk + %(prog)s twitter search --query "AI agents" --count 10 + %(prog)s search web --query "latest AI news" + %(prog)s search scholar --query "transformer architecture" + %(prog)s llm complete --model gpt-4 --prompt "Explain quantum computing" + """ + ) + + subparsers = parser.add_subparsers(dest="command", help="API category") + + # Twitter commands + twitter_parser = subparsers.add_parser("twitter", help="Twitter/X API operations") + twitter_sub = twitter_parser.add_subparsers(dest="action", help="Twitter action") + + # twitter user-info + user_info = twitter_sub.add_parser("user-info", help="Get user information") + user_info.add_argument("--username", "-u", required=True, help="Twitter username") + + # twitter tweets + tweets = twitter_sub.add_parser("tweets", help="Get user's last tweets") + tweets.add_argument("--username", "-u", required=True, help="Twitter username") + + # twitter search + search = twitter_sub.add_parser("search", help="Advanced tweet search") + search.add_argument("--query", "-q", required=True, help="Search query") + search.add_argument("--type", "-t", choices=["Latest", "Top"], default="Latest", help="Query type") + + # twitter detail + detail = twitter_sub.add_parser("detail", help="Get tweets by IDs") + detail.add_argument("--tweet-ids", "-t", required=True, help="Tweet IDs (comma-separated)") + + # twitter trends + trends = twitter_sub.add_parser("trends", help="Get trending topics") + trends.add_argument("--woeid", "-w", type=int, default=1, help="WOEID (1=worldwide)") + + # twitter user-search + user_search = twitter_sub.add_parser("user-search", help="Search for users") + user_search.add_argument("--keyword", "-k", required=True, help="Search keyword") + + # twitter login (V3) + login = twitter_sub.add_parser("login", help="Login to Twitter account (required for posting)") + login.add_argument("--username", "-u", required=True, help="Twitter username") + login.add_argument("--email", "-e", required=True, help="Account email") + login.add_argument("--password", "-p", required=True, help="Account password") + login.add_argument("--proxy", required=True, help="Proxy URL (http://user:pass@ip:port)") + login.add_argument("--totp", help="TOTP 2FA secret (recommended)") + + # twitter account (check login status) + account = twitter_sub.add_parser("account", help="Check logged-in account status") + account.add_argument("--username", "-u", required=True, help="Twitter username") + + # twitter post (send tweet) + post = twitter_sub.add_parser("post", help="Send a tweet (requires login)") + post.add_argument("--username", "-u", required=True, help="Twitter username") + post.add_argument("--text", "-t", required=True, help="Tweet text content") + post.add_argument("--media", help="Base64 encoded media data") + post.add_argument("--media-type", choices=["image/jpeg", "image/png", "image/gif", "video/mp4"], help="Media MIME type") + post.add_argument("--community", help="Community ID to post to") + + # twitter like + like = twitter_sub.add_parser("like", help="Like a tweet (requires login)") + like.add_argument("--username", "-u", required=True, help="Twitter username") + like.add_argument("--tweet-id", "-t", required=True, help="Tweet ID to like") + + # twitter retweet + retweet = twitter_sub.add_parser("retweet", help="Retweet a tweet (requires login)") + retweet.add_argument("--username", "-u", required=True, help="Twitter username") + retweet.add_argument("--tweet-id", "-t", required=True, help="Tweet ID to retweet") + + # Search commands + search_parser = subparsers.add_parser("search", help="Search API operations") + search_sub = search_parser.add_subparsers(dest="action", help="Search type") + + # search web + web_search = search_sub.add_parser("web", help="Web search") + web_search.add_argument("--query", "-q", required=True, help="Search query") + web_search.add_argument("--count", "-c", type=int, default=10, help="Max results (up to 100)") + + # search scholar + scholar_search = search_sub.add_parser("scholar", help="Academic paper search") + scholar_search.add_argument("--query", "-q", required=True, help="Search query") + scholar_search.add_argument("--count", "-c", type=int, default=10, help="Max results (up to 100)") + scholar_search.add_argument("--year-from", type=int, help="Publication year lower bound") + scholar_search.add_argument("--year-to", type=int, help="Publication year upper bound") + + # search smart + smart_search = search_sub.add_parser("smart", help="Smart search (web + academic)") + smart_search.add_argument("--query", "-q", required=True, help="Search query") + smart_search.add_argument("--count", "-c", type=int, default=10, help="Max results") + + # News commands + news_parser = subparsers.add_parser("news", help="Company news by ticker") + news_parser.add_argument("--ticker", "-t", required=True, help="Stock ticker (e.g., AAPL)") + news_parser.add_argument("--count", "-c", type=int, default=10, help="Number of results") + + # LLM commands + llm_parser = subparsers.add_parser("llm", help="LLM API operations") + llm_sub = llm_parser.add_subparsers(dest="action", help="LLM action") + + # llm complete + complete = llm_sub.add_parser("complete", help="Generate completion") + complete.add_argument("--model", "-m", required=True, help="Model name (e.g., gpt-4, claude-3)") + complete.add_argument("--prompt", "-p", required=True, help="Prompt text") + complete.add_argument("--max-tokens", type=int, help="Maximum tokens to generate") + complete.add_argument("--temperature", type=float, help="Sampling temperature") + + # llm chat + chat = llm_sub.add_parser("chat", help="Chat completion") + chat.add_argument("--model", "-m", required=True, help="Model name") + chat.add_argument("--messages", required=True, help="JSON array of messages") + chat.add_argument("--max-tokens", type=int, help="Maximum tokens to generate") + chat.add_argument("--temperature", type=float, help="Sampling temperature") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + try: + client = AIsaClient() + except ValueError as e: + print(json.dumps({"success": False, "error": {"code": "AUTH_ERROR", "message": str(e)}})) + sys.exit(1) + + result = None + + # Execute the appropriate command + if args.command == "twitter": + if args.action == "user-info": + result = client.twitter_user_info(args.username) + elif args.action == "tweets": + result = client.twitter_user_tweets(args.username) + elif args.action == "search": + result = client.twitter_search(args.query, args.type) + elif args.action == "detail": + result = client.twitter_tweet_detail(args.tweet_ids) + elif args.action == "trends": + result = client.twitter_trends(args.woeid) + elif args.action == "user-search": + result = client.twitter_user_search(args.keyword) + # V3 APIs (require login) + elif args.action == "login": + result = client.twitter_login(args.username, args.email, args.password, args.proxy, args.totp) + elif args.action == "account": + result = client.twitter_get_account(args.username) + elif args.action == "post": + result = client.twitter_send_tweet(args.username, args.text, args.media, args.media_type, args.community) + elif args.action == "like": + result = client.twitter_like(args.username, args.tweet_id) + elif args.action == "retweet": + result = client.twitter_retweet(args.username, args.tweet_id) + else: + twitter_parser.print_help() + sys.exit(1) + + elif args.command == "search": + if args.action == "web": + result = client.search_web(args.query, args.count) + elif args.action == "scholar": + year_from = getattr(args, 'year_from', None) + year_to = getattr(args, 'year_to', None) + result = client.search_scholar(args.query, args.count, year_from, year_to) + elif args.action == "smart": + result = client.search_smart(args.query, args.count) + else: + search_parser.print_help() + sys.exit(1) + + elif args.command == "news": + result = client.news(args.ticker, args.count) + + elif args.command == "llm": + kwargs = {} + if hasattr(args, "max_tokens") and args.max_tokens: + kwargs["max_tokens"] = args.max_tokens + if hasattr(args, "temperature") and args.temperature is not None: + kwargs["temperature"] = args.temperature + + if args.action == "complete": + result = client.llm_complete(args.model, args.prompt, **kwargs) + elif args.action == "chat": + try: + messages = json.loads(args.messages) + except json.JSONDecodeError: + print(json.dumps({"success": False, "error": {"code": "INVALID_JSON", "message": "Invalid JSON in --messages"}})) + sys.exit(1) + result = client.llm_chat(args.model, messages, **kwargs) + else: + llm_parser.print_help() + sys.exit(1) + + # Output result + if result: + # Handle encoding for Windows console + output = json.dumps(result, indent=2, ensure_ascii=False) + try: + print(output) + except UnicodeEncodeError: + # Fallback to ASCII-safe output + print(json.dumps(result, indent=2, ensure_ascii=True)) + sys.exit(0 if result.get("success", True) else 1) + + +if __name__ == "__main__": + main()