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.
- package/CONTRIBUTING.md +346 -0
- package/README.en.md +87 -0
- package/README.ja.md +87 -0
- package/README.ko.md +87 -0
- package/README.md +328 -0
- package/README.zh-CN.md +87 -0
- package/README.zh-TW.md +87 -0
- package/docs/EXAMPLES.md +681 -0
- package/docs/FAQ.md +479 -0
- package/felo-search/LICENSE +21 -0
- package/felo-search/README.md +495 -0
- package/felo-search/SKILL.md +292 -0
- package/package.json +33 -0
- package/src/cli.js +148 -0
- package/src/config.js +66 -0
- package/src/search.js +142 -0
- package/src/slides.js +228 -0
- package/tests/config.test.js +78 -0
- package/tests/search.test.js +100 -0
|
@@ -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
|
+
}
|