felo-ai 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ ---
2
+ name: felo-search
3
+ description: "Felo AI real-time web search for questions requiring current/live information. Triggers on current events, news, trends, real-time data, information queries, location queries, how-to guides, shopping, or when Claude's knowledge may be outdated."
4
+ ---
5
+
6
+ # Felo Search Skill
7
+
8
+ ## When to Use
9
+
10
+ Trigger this skill for questions requiring current or real-time information:
11
+
12
+ - **Current events & news:** Recent developments, trending topics, breaking news
13
+ - **Real-time data:** Weather, stock prices, exchange rates, sports scores
14
+ - **Information queries:** "What is...", "Tell me about...", product reviews, comparisons, recommendations
15
+ - **Location-based:** Restaurants, travel destinations, local attractions, things to do
16
+ - **How-to guides:** Tutorials, step-by-step instructions, best practices
17
+ - **Shopping & prices:** Product prices, deals, "where to buy"
18
+ - **Trends & statistics:** Market trends, rankings, data analysis
19
+ - **Any question where Claude's knowledge may be outdated**
20
+
21
+ **Trigger words:**
22
+ - 简体中文: 最近、什么、哪里、怎么样、如何、查、搜、找、推荐、比较、新闻、天气
23
+ - 繁體中文: 最近、什麼、哪裡、怎麼樣、如何、查、搜、找、推薦、比較、新聞、天氣
24
+ - 日本語: 最近、何、どこ、どう、検索、探す、おすすめ、比較、ニュース、天気
25
+ - English: latest, recent, what, where, how, best, search, find, compare, news, weather
26
+
27
+ **Explicit commands:** `/felo-search`, "search with felo", "felo search"
28
+
29
+ **Do NOT use for:**
30
+ - Code questions about the user's codebase (unless asking about external libraries/docs)
31
+ - Pure mathematical calculations or logical reasoning
32
+ - Questions about files in the current project
33
+
34
+ ## Setup
35
+
36
+ ### 1. Get Your API Key
37
+
38
+ 1. Visit [felo.ai](https://felo.ai) and log in (or register)
39
+ 2. Click your avatar in the top right corner → Settings
40
+ 3. Navigate to the "API Keys" tab
41
+ 4. Click "Create New Key" to generate a new API Key
42
+ 5. Copy and save your API Key securely
43
+
44
+ ### 2. Configure API Key
45
+
46
+ Set the `FELO_API_KEY` environment variable:
47
+
48
+ **Linux/macOS:**
49
+ ```bash
50
+ export FELO_API_KEY="your-api-key-here"
51
+ ```
52
+
53
+ **Windows (PowerShell):**
54
+ ```powershell
55
+ $env:FELO_API_KEY="your-api-key-here"
56
+ ```
57
+
58
+ **Windows (CMD):**
59
+ ```cmd
60
+ set FELO_API_KEY=your-api-key-here
61
+ ```
62
+
63
+ For permanent configuration, add it to your shell profile (~/.bashrc, ~/.zshrc) or system environment variables.
64
+
65
+ ## How to Execute
66
+
67
+ When this skill is triggered, execute the following steps using the Bash tool:
68
+
69
+ ### Step 1: Check API Key
70
+
71
+ Use the Bash tool to verify the API key is set:
72
+
73
+ ```bash
74
+ if [ -z "$FELO_API_KEY" ]; then
75
+ echo "ERROR: FELO_API_KEY not set"
76
+ exit 1
77
+ fi
78
+ echo "API key configured"
79
+ ```
80
+
81
+ If the API key is not set, inform the user with setup instructions and STOP.
82
+
83
+ ### Step 2: Make API Request
84
+
85
+ Extract the user's query and call the Felo API using a temporary JSON file to handle special characters:
86
+
87
+ ```bash
88
+ # Create query JSON (replace USER_QUERY with actual query)
89
+ cat > /tmp/felo_query.json << 'EOF'
90
+ {"query": "USER_QUERY_HERE"}
91
+ EOF
92
+
93
+ # Call Felo API
94
+ curl -s -X POST https://openapi.felo.ai/v2/chat \
95
+ -H "Authorization: Bearer $FELO_API_KEY" \
96
+ -H "Content-Type: application/json" \
97
+ -d @/tmp/felo_query.json
98
+
99
+ # Clean up
100
+ rm -f /tmp/felo_query.json
101
+ ```
102
+
103
+ **Notes:**
104
+ - Replace `USER_QUERY_HERE` with the actual user query
105
+ - Use heredoc (`cat > file << 'EOF'`) to properly handle Chinese, Japanese, and special characters
106
+ - Use `-s` flag with curl for clean output
107
+
108
+ ### Step 3: Parse and Format Response
109
+
110
+ The API returns JSON with this structure:
111
+ ```json
112
+ {
113
+ "answer": "AI-generated answer text",
114
+ "query_analysis": ["optimized query 1", "optimized query 2"]
115
+ }
116
+ ```
117
+
118
+ Parse the JSON response and present it to the user in this format:
119
+
120
+ ```
121
+ ## 回答 / Answer
122
+ [Display the answer field]
123
+
124
+ ## 搜索分析 / Query Analysis
125
+ 优化后的搜索词: [list query_analysis items]
126
+ ```
127
+
128
+ ## Complete Examples
129
+
130
+ ### Example 1: English Query
131
+
132
+ **User asks:** "What's the weather in Tokyo today?"
133
+
134
+ **Expected response format:**
135
+ ```
136
+ ## Answer
137
+ Tokyo weather today: Sunny, 22°C (72°F). High of 25°C, low of 18°C.
138
+ Light winds from the east at 10 km/h. UV index: 6 (high).
139
+ Good day for outdoor activities!
140
+
141
+ ## Query Analysis
142
+ Optimized search terms: Tokyo weather today, 東京 天気 今日
143
+ ```
144
+
145
+ **Bash command:**
146
+ ```bash
147
+ cat > /tmp/felo_query.json << 'EOF'
148
+ {"query": "What's the weather in Tokyo today?"}
149
+ EOF
150
+
151
+ curl -s -X POST https://openapi.felo.ai/v2/chat \
152
+ -H "Authorization: Bearer $FELO_API_KEY" \
153
+ -H "Content-Type: application/json" \
154
+ -d @/tmp/felo_query.json
155
+
156
+ rm -f /tmp/felo_query.json
157
+ ```
158
+
159
+ ### Example 2: Simplified Chinese (简体中文)
160
+
161
+ **User asks:** "杭州最近有什么新鲜事?"
162
+
163
+ **Expected response format:**
164
+ ```
165
+ ## 回答
166
+ 杭州最近的新鲜事包括:亚运会场馆改造完成、西湖景区推出夜游项目、
167
+ 新的地铁线路开通等。详细信息...
168
+
169
+ ## 搜索分析
170
+ 优化后的搜索词: 杭州最近新闻, 杭州近期动态, Hangzhou recent news
171
+ ```
172
+
173
+ **Bash command:**
174
+ ```bash
175
+ cat > /tmp/felo_query.json << 'EOF'
176
+ {"query": "杭州最近有什么新鲜事"}
177
+ EOF
178
+
179
+ curl -s -X POST https://openapi.felo.ai/v2/chat \
180
+ -H "Authorization: Bearer $FELO_API_KEY" \
181
+ -H "Content-Type: application/json" \
182
+ -d @/tmp/felo_query.json
183
+
184
+ rm -f /tmp/felo_query.json
185
+ ```
186
+
187
+ ### Example 3: Traditional Chinese - Taiwan (繁體中文-台灣)
188
+
189
+ **User asks:** "台北最近有什麼好玩的地方?"
190
+
191
+ **Bash command:**
192
+ ```bash
193
+ cat > /tmp/felo_query.json << 'EOF'
194
+ {"query": "台北最近有什麼好玩的地方"}
195
+ EOF
196
+
197
+ curl -s -X POST https://openapi.felo.ai/v2/chat \
198
+ -H "Authorization: Bearer $FELO_API_KEY" \
199
+ -H "Content-Type: application/json" \
200
+ -d @/tmp/felo_query.json
201
+
202
+ rm -f /tmp/felo_query.json
203
+ ```
204
+
205
+ ### Example 4: Japanese (日本語)
206
+
207
+ **User asks:** "東京で今人気のレストランは?"
208
+
209
+ **Bash command:**
210
+ ```bash
211
+ cat > /tmp/felo_query.json << 'EOF'
212
+ {"query": "東京で今人気のレストランは"}
213
+ EOF
214
+
215
+ curl -s -X POST https://openapi.felo.ai/v2/chat \
216
+ -H "Authorization: Bearer $FELO_API_KEY" \
217
+ -H "Content-Type: application/json" \
218
+ -d @/tmp/felo_query.json
219
+
220
+ rm -f /tmp/felo_query.json
221
+ ```
222
+
223
+ ## Error Handling
224
+
225
+ ### Common Error Codes
226
+
227
+ - `INVALID_API_KEY` - API Key is invalid or revoked
228
+ - Solution: Check if your API key is correct and hasn't been revoked
229
+ - `MISSING_PARAMETER` - Required parameter is missing
230
+ - Solution: Ensure the query parameter is provided
231
+ - `INVALID_PARAMETER` - Parameter value is invalid
232
+ - Solution: Check the query format
233
+ - `CHAT_FAILED` - Internal service error
234
+ - Solution: Retry the request or contact Felo support
235
+
236
+ ### Missing API Key
237
+
238
+ If `FELO_API_KEY` is not set, display this message:
239
+
240
+ ```
241
+ ❌ Felo API Key not configured
242
+
243
+ To use this skill, you need to set up your Felo API Key:
244
+
245
+ 1. Get your API key from https://felo.ai (Settings → API Keys)
246
+ 2. Set the environment variable:
247
+
248
+ Linux/macOS:
249
+ export FELO_API_KEY="your-api-key-here"
250
+
251
+ Windows (PowerShell):
252
+ $env:FELO_API_KEY="your-api-key-here"
253
+
254
+ 3. Restart Claude Code or reload the environment
255
+ ```
256
+
257
+ ## API Configuration
258
+
259
+ **Endpoint:** `https://openapi.felo.ai/v2/chat`
260
+
261
+ **Authentication:** Bearer token in Authorization header (from `FELO_API_KEY` environment variable)
262
+
263
+ **Request format:**
264
+ ```json
265
+ {
266
+ "query": "user's search query"
267
+ }
268
+ ```
269
+
270
+ **Response format:**
271
+ ```json
272
+ {
273
+ "answer": "AI-generated comprehensive answer",
274
+ "query_analysis": ["optimized query 1", "optimized query 2"]
275
+ }
276
+ ```
277
+
278
+ ## Important Notes
279
+
280
+ - This skill should be used for any question requiring current information
281
+ - Execute immediately using the Bash tool - don't just describe what you would do
282
+ - Multi-language support: Fully supports Simplified Chinese, Traditional Chinese (Taiwan), Japanese, and English
283
+ - Handle special characters properly: Use heredoc for JSON files to avoid encoding issues
284
+ - Parse JSON response: Extract answer and query_analysis fields
285
+ - Format nicely: Present results in a clean, readable format with proper markdown
286
+ - The API returns results in the same language as the query when possible
287
+
288
+ ## Additional Resources
289
+
290
+ - [Felo Open Platform Documentation](https://openapi.felo.ai)
291
+ - [Get API Key](https://felo.ai) (Settings → API Keys)
292
+ - [API Reference](https://openapi.felo.ai/docs)
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "felo-ai",
3
+ "version": "0.2.0",
4
+ "description": "Felo AI CLI - real-time search from the terminal",
5
+ "type": "module",
6
+ "main": "src/cli.js",
7
+ "bin": {
8
+ "felo": "src/cli.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "keywords": [
14
+ "felo",
15
+ "felo-ai",
16
+ "search",
17
+ "slides",
18
+ "cli",
19
+ "ai"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Felo-Inc/felo-skills.git"
25
+ },
26
+ "dependencies": {
27
+ "commander": "^12.0.0",
28
+ "felo-search": "^0.1.1"
29
+ },
30
+ "scripts": {
31
+ "test": "node --test tests/"
32
+ }
33
+ }
package/src/cli.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'module';
4
+ import { Command } from 'commander';
5
+ import { search } from './search.js';
6
+ import { slides } from './slides.js';
7
+ import * as config from './config.js';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require('../package.json');
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('felo')
16
+ .description('Felo AI CLI - real-time search from the terminal')
17
+ .version(pkg.version);
18
+
19
+ program
20
+ .command('search')
21
+ .description('Search for current information (weather, news, docs, etc.)')
22
+ .argument('<query>', 'search query')
23
+ .option('-j, --json', 'output raw JSON')
24
+ .option('-v, --verbose', 'show query analysis and sources')
25
+ .option('-t, --timeout <seconds>', 'request timeout in seconds', '60')
26
+ .action(async (query, opts) => {
27
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
28
+ const code = await search(query, {
29
+ json: opts.json,
30
+ verbose: opts.verbose,
31
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
32
+ });
33
+ process.exit(code);
34
+ });
35
+
36
+ program
37
+ .command('slides')
38
+ .description('Generate PPT/slides from a prompt (async task, outputs live doc URL when done)')
39
+ .argument('<query>', 'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")')
40
+ .option('-j, --json', 'output raw JSON with task_id and live_doc_url')
41
+ .option('-v, --verbose', 'show polling status')
42
+ .option('-t, --timeout <seconds>', 'request timeout in seconds for each API call', '60')
43
+ .option('--poll-timeout <seconds>', 'max seconds to wait for task completion', '600')
44
+ .action(async (query, opts) => {
45
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
46
+ const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 600000;
47
+ const code = await slides(query, {
48
+ json: opts.json,
49
+ verbose: opts.verbose,
50
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
51
+ pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 600000 : pollTimeoutMs,
52
+ });
53
+ process.exitCode = code;
54
+ // Defer exit so stderr can flush; reduces Node.js Windows assertion (UV_HANDLE_CLOSING)
55
+ setTimeout(() => process.exit(code), 0);
56
+ });
57
+
58
+ const configCmd = program
59
+ .command('config')
60
+ .description('Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json');
61
+
62
+ configCmd
63
+ .command('set <key> <value>')
64
+ .description('Set a config value (e.g. felo config set FELO_API_KEY your-key)')
65
+ .action(async (key, value) => {
66
+ try {
67
+ await config.setConfig(key, value);
68
+ console.log(`Set ${key}`);
69
+ } catch (e) {
70
+ console.error('Error:', e.message);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
75
+ configCmd
76
+ .command('get <key>')
77
+ .description('Get a config value (sensitive keys are masked)')
78
+ .action(async (key) => {
79
+ try {
80
+ const value = await config.getConfigValue(key);
81
+ if (value === undefined || value === null) {
82
+ console.log('(not set)');
83
+ } else {
84
+ console.log(config.maskValueForDisplay(key, value));
85
+ }
86
+ } catch (e) {
87
+ console.error('Error:', e.message);
88
+ process.exit(1);
89
+ }
90
+ });
91
+
92
+ configCmd
93
+ .command('list')
94
+ .description('List all config keys (values are hidden)')
95
+ .action(async () => {
96
+ try {
97
+ const c = await config.listConfig();
98
+ const keys = Object.keys(c);
99
+ if (keys.length === 0) {
100
+ console.log('No config set. Use: felo config set FELO_API_KEY <key>');
101
+ return;
102
+ }
103
+ keys.forEach((k) => console.log(k));
104
+ } catch (e) {
105
+ console.error('Error:', e.message);
106
+ process.exit(1);
107
+ }
108
+ });
109
+
110
+ configCmd
111
+ .command('unset <key>')
112
+ .description('Remove a config value')
113
+ .action(async (key) => {
114
+ try {
115
+ await config.unsetConfig(key);
116
+ console.log(`Unset ${key}`);
117
+ } catch (e) {
118
+ console.error('Error:', e.message);
119
+ process.exit(1);
120
+ }
121
+ });
122
+
123
+ configCmd
124
+ .command('path')
125
+ .description('Show config file path')
126
+ .action(() => {
127
+ console.log(config.getConfigPath());
128
+ });
129
+
130
+ program
131
+ .command('summarize')
132
+ .description('Summarize text or URL (coming when API is available)')
133
+ .argument('[input]', 'text or URL to summarize')
134
+ .action(() => {
135
+ console.error('summarize: not yet implemented. Use felo search for now.');
136
+ process.exit(1);
137
+ });
138
+
139
+ program
140
+ .command('translate')
141
+ .description('Translate text (coming when API is available)')
142
+ .argument('[text]', 'text to translate')
143
+ .action(() => {
144
+ console.error('translate: not yet implemented. Use felo search for now.');
145
+ process.exit(1);
146
+ });
147
+
148
+ program.parse();
package/src/config.js ADDED
@@ -0,0 +1,66 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ function getConfigFilePath() {
6
+ return process.env.FELO_CONFIG_FILE || path.join(os.homedir(), '.felo', 'config.json');
7
+ }
8
+
9
+ const SENSITIVE_KEYS = ['FELO_API_KEY', 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD'];
10
+
11
+ async function ensureConfigDir() {
12
+ await fs.mkdir(path.dirname(getConfigFilePath()), { recursive: true });
13
+ }
14
+
15
+ export async function getConfig() {
16
+ const configFile = getConfigFilePath();
17
+ try {
18
+ const raw = await fs.readFile(configFile, 'utf-8');
19
+ return JSON.parse(raw);
20
+ } catch (e) {
21
+ if (e.code === 'ENOENT') return {};
22
+ if (e instanceof SyntaxError) {
23
+ process.stderr.write('Warning: Invalid config file, using empty config.\n');
24
+ return {};
25
+ }
26
+ throw e;
27
+ }
28
+ }
29
+
30
+ export async function setConfig(key, value) {
31
+ await ensureConfigDir();
32
+ const config = await getConfig();
33
+ config[key] = value;
34
+ await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
35
+ }
36
+
37
+ export async function getConfigValue(key) {
38
+ const config = await getConfig();
39
+ return config[key];
40
+ }
41
+
42
+ /** Returns value suitable for display; masks sensitive keys. */
43
+ export function maskValueForDisplay(key, value) {
44
+ if (value === undefined || value === null) return value;
45
+ const s = String(value).trim();
46
+ if (!s) return s;
47
+ const upper = key.toUpperCase();
48
+ const isSensitive = SENSITIVE_KEYS.some((k) => upper === k || upper.endsWith('_' + k));
49
+ if (!isSensitive || s.length <= 8) return s;
50
+ return s.slice(0, 4) + '...' + s.slice(-4);
51
+ }
52
+
53
+ export async function unsetConfig(key) {
54
+ const config = await getConfig();
55
+ delete config[key];
56
+ await ensureConfigDir();
57
+ await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
58
+ }
59
+
60
+ export async function listConfig() {
61
+ return getConfig();
62
+ }
63
+
64
+ export function getConfigPath() {
65
+ return getConfigFilePath();
66
+ }
package/src/search.js ADDED
@@ -0,0 +1,142 @@
1
+ const FELO_API = 'https://openapi.felo.ai/v2/chat';
2
+ const DEFAULT_TIMEOUT_MS = 60_000;
3
+ const MAX_RETRIES = 3;
4
+ const RETRY_BASE_MS = 1000;
5
+
6
+ const NO_KEY_MESSAGE = `
7
+ ❌ Felo API Key not configured
8
+
9
+ To use Felo CLI, set the FELO_API_KEY environment variable or run:
10
+
11
+ felo config set FELO_API_KEY <your-api-key>
12
+
13
+ Get your API key from https://felo.ai (Settings → API Keys).
14
+ `;
15
+
16
+ async function getApiKey() {
17
+ if (process.env.FELO_API_KEY?.trim()) {
18
+ return process.env.FELO_API_KEY.trim();
19
+ }
20
+ const { getConfigValue } = await import('./config.js');
21
+ const fromConfig = await getConfigValue('FELO_API_KEY');
22
+ return typeof fromConfig === 'string' ? fromConfig.trim() : '';
23
+ }
24
+
25
+ export { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE };
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
32
+ let lastError;
33
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
34
+ const controller = new AbortController();
35
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const res = await fetch(url, {
38
+ ...options,
39
+ signal: controller.signal,
40
+ });
41
+ clearTimeout(timeoutId);
42
+ // Retry on 5xx (server errors)
43
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
44
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt);
45
+ await sleep(delay);
46
+ continue;
47
+ }
48
+ return res;
49
+ } catch (err) {
50
+ clearTimeout(timeoutId);
51
+ lastError = err;
52
+ if (err.name === 'AbortError') {
53
+ throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
54
+ }
55
+ if (attempt < MAX_RETRIES) {
56
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt);
57
+ await sleep(delay);
58
+ continue;
59
+ }
60
+ throw lastError;
61
+ }
62
+ }
63
+ throw lastError;
64
+ }
65
+
66
+ export async function search(query, options = {}) {
67
+ const apiKey = await getApiKey();
68
+ if (!apiKey) {
69
+ console.error(NO_KEY_MESSAGE.trim());
70
+ return 1;
71
+ }
72
+
73
+ try {
74
+ process.stderr.write('Searching...\n');
75
+
76
+ const res = await fetchWithTimeoutAndRetry(
77
+ FELO_API,
78
+ {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Authorization': `Bearer ${apiKey}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify({ query: query.trim() }),
85
+ },
86
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
87
+ );
88
+
89
+ const data = await res.json().catch(() => ({}));
90
+
91
+ // API error response: { status: "error", code, message } (per doc)
92
+ if (data.status === 'error') {
93
+ const msg = data.message || data.code || 'Unknown error';
94
+ console.error(`Error: ${msg}`);
95
+ return 1;
96
+ }
97
+
98
+ if (!res.ok) {
99
+ const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
100
+ console.error(`Error: ${msg}`);
101
+ return 1;
102
+ }
103
+
104
+ // Success: { status: "ok", data: { answer, query_analysis: { queries }, resources } }
105
+ const payload = data.data;
106
+ if (!payload) {
107
+ console.error('Error: Unexpected response format');
108
+ return 1;
109
+ }
110
+
111
+ if (options.json) {
112
+ console.log(JSON.stringify(data, null, 2));
113
+ return 0;
114
+ }
115
+
116
+ // Default: only the answer (stdout, pipe-friendly)
117
+ if (payload.answer) {
118
+ console.log(payload.answer);
119
+ }
120
+
121
+ // Verbose: add query analysis and sources (per API doc)
122
+ if (options.verbose) {
123
+ const queries = payload.query_analysis?.queries;
124
+ if (Array.isArray(queries) && queries.length) {
125
+ process.stderr.write('\n## Query Analysis\n');
126
+ process.stderr.write(`Optimized search terms: ${queries.join(', ')}\n`);
127
+ }
128
+ const resources = payload.resources;
129
+ if (Array.isArray(resources) && resources.length) {
130
+ process.stderr.write('\n## Sources\n');
131
+ resources.forEach((r) => {
132
+ process.stderr.write(`- ${r.title}: ${r.link}\n`);
133
+ });
134
+ }
135
+ }
136
+
137
+ return 0;
138
+ } catch (err) {
139
+ console.error('Error:', err.message || err);
140
+ return 1;
141
+ }
142
+ }