felo-ai 0.2.12 → 0.2.14
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/README.md +113 -374
- package/felo-livedoc/LICENSE +21 -0
- package/felo-livedoc/README.md +53 -0
- package/felo-livedoc/SKILL.md +180 -0
- package/felo-livedoc/scripts/run_livedoc.mjs +391 -0
- package/package.json +4 -2
- package/src/cli.js +205 -0
- package/src/livedoc.js +401 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Felo LiveDoc
|
|
2
|
+
|
|
3
|
+
Manage knowledge bases (LiveDocs) and their resources via the Felo API.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- Create, list, update, and delete knowledge bases (LiveDocs)
|
|
10
|
+
- Add resources: text documents, URLs, file uploads
|
|
11
|
+
- Semantic retrieval across knowledge base resources
|
|
12
|
+
- Full CRUD for resources within a LiveDoc
|
|
13
|
+
|
|
14
|
+
**When to use:**
|
|
15
|
+
- Building or managing a knowledge base
|
|
16
|
+
- Uploading documents or URLs for AI-powered retrieval
|
|
17
|
+
- Searching across your knowledge base with natural language
|
|
18
|
+
|
|
19
|
+
**When NOT to use:**
|
|
20
|
+
- General web search (use `felo-search`)
|
|
21
|
+
- PPT generation (use `felo-slides`)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Setup
|
|
26
|
+
|
|
27
|
+
### Step 1: Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude install Felo-Inc/felo-skills/felo-livedoc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Step 2: Configure API Key
|
|
34
|
+
|
|
35
|
+
Get your API key from [felo.ai](https://felo.ai) (Settings → API Keys), then:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export FELO_API_KEY="your-api-key-here"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Step 3: Test
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
felo livedoc list
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage Examples
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Create a knowledge base
|
|
53
|
+
felo livedoc create --name "My KB" --description "Project docs"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: felo-livedoc
|
|
3
|
+
description: "Manage Felo LiveDocs (knowledge bases) and their resources. Use when users want to create, manage, or query knowledge bases, upload documents, add URLs, or perform semantic retrieval over a knowledge base. Explicit commands: /felo-livedoc."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Felo LiveDoc Skill
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Trigger this skill when users want to:
|
|
11
|
+
|
|
12
|
+
- **Create/manage knowledge bases:** Create, list, update, or delete LiveDocs
|
|
13
|
+
- **Add resources:** Upload documents, add URLs, or create text documents in a LiveDoc
|
|
14
|
+
- **Semantic retrieval:** Search across knowledge base resources using natural language queries
|
|
15
|
+
- **Resource management:** List, view, or delete resources within a LiveDoc
|
|
16
|
+
|
|
17
|
+
**Trigger words:**
|
|
18
|
+
- English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval
|
|
19
|
+
- 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索, 知识检索
|
|
20
|
+
|
|
21
|
+
**Explicit commands:** `/felo-livedoc`, "livedoc", "felo livedoc"
|
|
22
|
+
|
|
23
|
+
**Do NOT use for:**
|
|
24
|
+
- General web search (use felo-search)
|
|
25
|
+
- PPT generation (use felo-slides)
|
|
26
|
+
- SuperAgent conversations (use felo-superAgent)
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### 1. Get Your API Key
|
|
31
|
+
|
|
32
|
+
1. Visit [felo.ai](https://felo.ai) and log in (or register)
|
|
33
|
+
2. Click your avatar in the top right corner → Settings
|
|
34
|
+
3. Navigate to the "API Keys" tab
|
|
35
|
+
4. Click "Create New Key" to generate a new API Key
|
|
36
|
+
5. Copy and save your API Key securely
|
|
37
|
+
|
|
38
|
+
### 2. Configure API Key
|
|
39
|
+
|
|
40
|
+
Set the `FELO_API_KEY` environment variable:
|
|
41
|
+
|
|
42
|
+
**Linux/macOS:**
|
|
43
|
+
```bash
|
|
44
|
+
export FELO_API_KEY="your-api-key-here"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Windows (PowerShell):**
|
|
48
|
+
```powershell
|
|
49
|
+
$env:FELO_API_KEY="your-api-key-here"
|
|
50
|
+
```
|
|
51
|
+
## How to Execute
|
|
52
|
+
|
|
53
|
+
When this skill is triggered, execute the livedoc script using the Bash tool:
|
|
54
|
+
|
|
55
|
+
### LiveDoc Management
|
|
56
|
+
|
|
57
|
+
**Create a LiveDoc:**
|
|
58
|
+
```bash
|
|
59
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs create --name "KB Name" --description "Description"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**List LiveDocs:**
|
|
63
|
+
```bash
|
|
64
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs list
|
|
65
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs list --keyword "search term"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Update a LiveDoc:**
|
|
69
|
+
```bash
|
|
70
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update SHORT_ID --name "New Name" --description "New Desc"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Delete a LiveDoc:**
|
|
74
|
+
```bash
|
|
75
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs delete SHORT_ID
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Resource Management
|
|
79
|
+
|
|
80
|
+
**Add a text document:**
|
|
81
|
+
```bash
|
|
82
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-doc SHORT_ID --title "Doc Title" --content "Document content here"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Add URLs (max 10, comma-separated):**
|
|
86
|
+
```bash
|
|
87
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-urls SHORT_ID --urls "https://example.com,https://example.org"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Upload a file:**
|
|
91
|
+
```bash
|
|
92
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs upload SHORT_ID --file ./document.pdf
|
|
93
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs upload SHORT_ID --file ./document.pdf --convert
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**List resources:**
|
|
97
|
+
```bash
|
|
98
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs resources SHORT_ID
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Get a single resource:**
|
|
102
|
+
```bash
|
|
103
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs resource SHORT_ID RESOURCE_ID
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Delete a resource:**
|
|
107
|
+
```bash
|
|
108
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs remove-resource SHORT_ID RESOURCE_ID
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Semantic Retrieval
|
|
112
|
+
|
|
113
|
+
**Search across resources:**
|
|
114
|
+
```bash
|
|
115
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs retrieve SHORT_ID --query "your search query"
|
|
116
|
+
```
|
|
117
|
+
### Options
|
|
118
|
+
|
|
119
|
+
All commands support:
|
|
120
|
+
- `--json` or `-j` — output raw JSON response
|
|
121
|
+
- `--timeout <ms>` or `-t <ms>` — request timeout in milliseconds (default: 60000)
|
|
122
|
+
|
|
123
|
+
### Parse and Format Response
|
|
124
|
+
|
|
125
|
+
The API returns JSON with this structure:
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"status": "ok",
|
|
129
|
+
"message": "success",
|
|
130
|
+
"data": { ... }
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**LiveDoc object:**
|
|
135
|
+
- `short_id` — unique identifier (use this for all operations)
|
|
136
|
+
- `name` — LiveDoc name
|
|
137
|
+
- `description` — LiveDoc description
|
|
138
|
+
- `created_at` / `modified_at` — timestamps
|
|
139
|
+
|
|
140
|
+
**Resource object:**
|
|
141
|
+
- `id` — resource identifier
|
|
142
|
+
- `title` — resource title
|
|
143
|
+
- `resource_type` — type (web, ai_doc, file, etc.)
|
|
144
|
+
- `status` — processing status
|
|
145
|
+
- `snippet` — content preview
|
|
146
|
+
|
|
147
|
+
**Retrieve result:**
|
|
148
|
+
- `id` — resource ID
|
|
149
|
+
- `title` — resource title
|
|
150
|
+
- `content` — matched content
|
|
151
|
+
- `score` — relevance score (0-1)
|
|
152
|
+
|
|
153
|
+
## Error Handling
|
|
154
|
+
|
|
155
|
+
### Common Error Codes
|
|
156
|
+
|
|
157
|
+
- `INVALID_API_KEY` — API Key is invalid or revoked
|
|
158
|
+
- `LIVEDOC_NOT_FOUND` — LiveDoc does not exist
|
|
159
|
+
- `LIVEDOC_RESOURCE_NOT_FOUND` — Resource does not exist
|
|
160
|
+
- `LIVEDOC_CREATE_FAILED` — Failed to create LiveDoc
|
|
161
|
+
- `LIVEDOC_RESOURCE_UPLOAD_FAILED` — File upload failed
|
|
162
|
+
- `LIVEDOC_RESOURCE_ADD_URLS_FAILED` — URL addition failed
|
|
163
|
+
- `LIVEDOC_RESOURCE_RETRIEVE_FAILED` — Semantic retrieval failed
|
|
164
|
+
|
|
165
|
+
### Missing API Key
|
|
166
|
+
|
|
167
|
+
If `FELO_API_KEY` is not set, display this message:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
ERROR: FELO_API_KEY not set. Get your API key from https://felo.ai (Settings → API Keys).
|
|
171
|
+
Set it with: export FELO_API_KEY="your-key"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Important Notes
|
|
175
|
+
|
|
176
|
+
- Always use the `short_id` returned from create/list to reference a LiveDoc
|
|
177
|
+
- URL resources are limited to 10 per request
|
|
178
|
+
- Use `--convert` with upload to convert files to searchable documents
|
|
179
|
+
- Semantic retrieval returns results sorted by relevance score
|
|
180
|
+
- Execute immediately using the Bash tool — don't just describe what you would do
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
8
|
+
const MAX_RETRIES = 3;
|
|
9
|
+
const RETRY_BASE_MS = 1000;
|
|
10
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
11
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
12
|
+
const STATUS_PAD = 56;
|
|
13
|
+
|
|
14
|
+
function startSpinner(message) {
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
let i = 0;
|
|
17
|
+
const id = setInterval(() => {
|
|
18
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
19
|
+
const line = `${message} ${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${elapsed}s`;
|
|
20
|
+
process.stderr.write(`\r${line.padEnd(STATUS_PAD, ' ')}`);
|
|
21
|
+
i += 1;
|
|
22
|
+
}, SPINNER_INTERVAL_MS);
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stopSpinner(id) {
|
|
27
|
+
if (id != null) clearInterval(id);
|
|
28
|
+
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
32
|
+
|
|
33
|
+
function getMessage(p) {
|
|
34
|
+
return p?.message || p?.error || p?.msg || p?.code || 'Unknown error';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchWithRetry(url, init, timeoutMs) {
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
44
|
+
if (res.status >= 500 && attempt < MAX_RETRIES) {
|
|
45
|
+
await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return res;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
lastError = err;
|
|
51
|
+
if (err.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
|
|
52
|
+
if (attempt < MAX_RETRIES) { await sleep(RETRY_BASE_MS * Math.pow(2, attempt)); continue; }
|
|
53
|
+
throw lastError;
|
|
54
|
+
} finally { clearTimeout(timer); }
|
|
55
|
+
}
|
|
56
|
+
throw lastError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function apiRequest(method, apiPath, body, apiKey, apiBase, timeoutMs) {
|
|
60
|
+
const url = `${apiBase}/v2${apiPath}`;
|
|
61
|
+
const headers = { Accept: 'application/json', Authorization: `Bearer ${apiKey}` };
|
|
62
|
+
const init = { method, headers };
|
|
63
|
+
if (body !== undefined && body !== null) {
|
|
64
|
+
headers['Content-Type'] = 'application/json';
|
|
65
|
+
init.body = JSON.stringify(body);
|
|
66
|
+
}
|
|
67
|
+
const res = await fetchWithRetry(url, init, timeoutMs);
|
|
68
|
+
let data = {};
|
|
69
|
+
try { data = await res.json(); } catch { data = {}; }
|
|
70
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
|
|
71
|
+
if (data.status === 'error') throw new Error(getMessage(data));
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function uploadFormData(apiPath, formData, apiKey, apiBase, timeoutMs) {
|
|
76
|
+
const url = `${apiBase}/v2${apiPath}`;
|
|
77
|
+
const res = await fetchWithRetry(
|
|
78
|
+
url,
|
|
79
|
+
{ method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${apiKey}` }, body: formData },
|
|
80
|
+
timeoutMs,
|
|
81
|
+
);
|
|
82
|
+
let data = {};
|
|
83
|
+
try { data = await res.json(); } catch { data = {}; }
|
|
84
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
|
|
85
|
+
if (data.status === 'error') throw new Error(getMessage(data));
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Formatting ──
|
|
90
|
+
|
|
91
|
+
function formatLiveDoc(doc) {
|
|
92
|
+
if (!doc) return '';
|
|
93
|
+
let out = `## ${doc.name || '(untitled)'}\n`;
|
|
94
|
+
out += `- ID: \`${doc.short_id}\`\n`;
|
|
95
|
+
if (doc.description) out += `- Description: ${doc.description}\n`;
|
|
96
|
+
if (doc.icon) out += `- Icon: ${doc.icon}\n`;
|
|
97
|
+
if (doc.created_at) out += `- Created: ${doc.created_at}\n`;
|
|
98
|
+
if (doc.modified_at) out += `- Modified: ${doc.modified_at}\n`;
|
|
99
|
+
out += '\n';
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatResource(r) {
|
|
104
|
+
if (!r) return '';
|
|
105
|
+
let out = `### ${r.title || '(untitled)'}\n`;
|
|
106
|
+
out += `- Resource ID: \`${r.id}\`\n`;
|
|
107
|
+
if (r.resource_type) out += `- Type: ${r.resource_type}\n`;
|
|
108
|
+
if (r.status) out += `- Status: ${r.status}\n`;
|
|
109
|
+
if (r.source) out += `- Source: ${r.source}\n`;
|
|
110
|
+
if (r.link) out += `- Link: ${r.link}\n`;
|
|
111
|
+
if (r.snippet) out += `- Snippet: ${r.snippet}\n`;
|
|
112
|
+
if (r.created_at) out += `- Created: ${r.created_at}\n`;
|
|
113
|
+
out += '\n';
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function formatRetrieveResult(r) {
|
|
117
|
+
if (!r) return '';
|
|
118
|
+
const score = r.score != null ? `${(r.score * 100).toFixed(1)}%` : 'N/A';
|
|
119
|
+
let out = `### ${r.title || '(untitled)'} (score: ${score})\n`;
|
|
120
|
+
out += `- ID: \`${r.id}\`\n`;
|
|
121
|
+
if (r.content) {
|
|
122
|
+
const preview = r.content.length > 300 ? r.content.slice(0, 300) + '...' : r.content;
|
|
123
|
+
out += `- Content: ${preview}\n`;
|
|
124
|
+
}
|
|
125
|
+
out += '\n';
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── CLI ──
|
|
130
|
+
|
|
131
|
+
function usage() {
|
|
132
|
+
console.error([
|
|
133
|
+
'Usage: node run_livedoc.mjs <action> [args] [options]',
|
|
134
|
+
'',
|
|
135
|
+
'Actions:',
|
|
136
|
+
' create Create a LiveDoc (--name required)',
|
|
137
|
+
' list List LiveDocs',
|
|
138
|
+
' update <short_id> Update a LiveDoc',
|
|
139
|
+
' delete <short_id> Delete a LiveDoc',
|
|
140
|
+
' resources <short_id> List resources',
|
|
141
|
+
' resource <short_id> <resource_id> Get a resource',
|
|
142
|
+
' add-doc <short_id> Create text document (--content required)',
|
|
143
|
+
' add-urls <short_id> Add URLs (--urls required, comma-separated, max 10)',
|
|
144
|
+
' upload <short_id> Upload file (--file required, --convert optional)',
|
|
145
|
+
' remove-resource <short_id> <resource_id> Delete a resource',
|
|
146
|
+
' retrieve <short_id> Semantic search (--query required)',
|
|
147
|
+
'',
|
|
148
|
+
'Options:',
|
|
149
|
+
' --name <name> LiveDoc name',
|
|
150
|
+
' --description <desc> LiveDoc description',
|
|
151
|
+
' --icon <icon> LiveDoc icon',
|
|
152
|
+
' --keyword <kw> Search keyword (list)',
|
|
153
|
+
' --page <n> Page number',
|
|
154
|
+
' --size <n> Page size',
|
|
155
|
+
' --type <type> Resource type filter',
|
|
156
|
+
' --content <text> Document content',
|
|
157
|
+
' --title <title> Document title',
|
|
158
|
+
' --urls <urls> Comma-separated URLs',
|
|
159
|
+
' --file <path> File path to upload',
|
|
160
|
+
' --convert Convert uploaded file to document',
|
|
161
|
+
' --query <text> Retrieval query',
|
|
162
|
+
' -j, --json Output raw JSON',
|
|
163
|
+
' -t, --timeout <ms> Timeout in ms (default: 60000)',
|
|
164
|
+
' --help Show this help',
|
|
165
|
+
].join('\n'));
|
|
166
|
+
}
|
|
167
|
+
function parseArgs(argv) {
|
|
168
|
+
const out = {
|
|
169
|
+
action: '', positional: [], name: '', description: '', icon: '',
|
|
170
|
+
keyword: '', page: '', size: '', type: '', content: '', title: '',
|
|
171
|
+
urls: '', file: '', convert: false, query: '',
|
|
172
|
+
json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
|
|
173
|
+
};
|
|
174
|
+
const positional = [];
|
|
175
|
+
for (let i = 0; i < argv.length; i++) {
|
|
176
|
+
const a = argv[i];
|
|
177
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
178
|
+
else if (a === '--json' || a === '-j') out.json = true;
|
|
179
|
+
else if (a === '--convert') out.convert = true;
|
|
180
|
+
else if (a === '--name') out.name = argv[++i] || '';
|
|
181
|
+
else if (a === '--description') out.description = argv[++i] || '';
|
|
182
|
+
else if (a === '--icon') out.icon = argv[++i] || '';
|
|
183
|
+
else if (a === '--keyword') out.keyword = argv[++i] || '';
|
|
184
|
+
else if (a === '--page') out.page = argv[++i] || '';
|
|
185
|
+
else if (a === '--size') out.size = argv[++i] || '';
|
|
186
|
+
else if (a === '--type') out.type = argv[++i] || '';
|
|
187
|
+
else if (a === '--content') out.content = argv[++i] || '';
|
|
188
|
+
else if (a === '--title') out.title = argv[++i] || '';
|
|
189
|
+
else if (a === '--urls') out.urls = argv[++i] || '';
|
|
190
|
+
else if (a === '--file') out.file = argv[++i] || '';
|
|
191
|
+
else if (a === '--query') out.query = argv[++i] || '';
|
|
192
|
+
else if (a === '-t' || a === '--timeout') {
|
|
193
|
+
const n = parseInt(argv[++i] || '', 10);
|
|
194
|
+
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
195
|
+
}
|
|
196
|
+
else if (!a.startsWith('-')) positional.push(a);
|
|
197
|
+
}
|
|
198
|
+
out.action = positional[0] || '';
|
|
199
|
+
out.positional = positional.slice(1);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function main() {
|
|
204
|
+
const args = parseArgs(process.argv.slice(2));
|
|
205
|
+
if (args.help || !args.action) { usage(); process.exit(args.help ? 0 : 1); }
|
|
206
|
+
|
|
207
|
+
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
208
|
+
if (!apiKey) { console.error('ERROR: FELO_API_KEY not set'); process.exit(1); }
|
|
209
|
+
|
|
210
|
+
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
211
|
+
const { action, positional, json, timeoutMs } = args;
|
|
212
|
+
const shortId = positional[0] || '';
|
|
213
|
+
const resourceId = positional[1] || '';
|
|
214
|
+
|
|
215
|
+
let code = 1;
|
|
216
|
+
let spinnerId;
|
|
217
|
+
try {
|
|
218
|
+
switch (action) {
|
|
219
|
+
case 'create': {
|
|
220
|
+
if (!args.name) { console.error('ERROR: --name is required'); break; }
|
|
221
|
+
spinnerId = startSpinner('Creating LiveDoc');
|
|
222
|
+
const body = { name: args.name };
|
|
223
|
+
if (args.description) body.description = args.description;
|
|
224
|
+
if (args.icon) body.icon = args.icon;
|
|
225
|
+
const payload = await apiRequest('POST', '/livedocs', body, apiKey, apiBase, timeoutMs);
|
|
226
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
227
|
+
else { process.stdout.write('LiveDoc created!\n\n'); process.stdout.write(formatLiveDoc(payload?.data)); }
|
|
228
|
+
code = 0;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'list': {
|
|
232
|
+
spinnerId = startSpinner('Listing LiveDocs');
|
|
233
|
+
const params = new URLSearchParams();
|
|
234
|
+
if (args.keyword) params.set('keyword', args.keyword);
|
|
235
|
+
if (args.page) params.set('page', args.page);
|
|
236
|
+
if (args.size) params.set('size', args.size);
|
|
237
|
+
const qs = params.toString();
|
|
238
|
+
const payload = await apiRequest('GET', `/livedocs${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
239
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
240
|
+
else {
|
|
241
|
+
const items = payload?.data?.items || [];
|
|
242
|
+
if (!items.length) { process.stderr.write('No LiveDocs found.\n'); }
|
|
243
|
+
else {
|
|
244
|
+
process.stdout.write(`Found ${payload.data.total || items.length} LiveDoc(s)\n\n`);
|
|
245
|
+
for (const doc of items) process.stdout.write(formatLiveDoc(doc));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
code = 0;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case 'update': {
|
|
252
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
253
|
+
spinnerId = startSpinner('Updating LiveDoc');
|
|
254
|
+
const body = {};
|
|
255
|
+
if (args.name) body.name = args.name;
|
|
256
|
+
if (args.description) body.description = args.description;
|
|
257
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}`, body, apiKey, apiBase, timeoutMs);
|
|
258
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
259
|
+
else { process.stdout.write('LiveDoc updated!\n\n'); process.stdout.write(formatLiveDoc(payload?.data)); }
|
|
260
|
+
code = 0;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'delete': {
|
|
264
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
265
|
+
spinnerId = startSpinner('Deleting LiveDoc');
|
|
266
|
+
await apiRequest('DELETE', `/livedocs/${shortId}`, null, apiKey, apiBase, timeoutMs);
|
|
267
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
268
|
+
else { process.stdout.write(`LiveDoc \`${shortId}\` deleted.\n`); }
|
|
269
|
+
code = 0;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'resources': {
|
|
273
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
274
|
+
spinnerId = startSpinner('Listing resources');
|
|
275
|
+
const params = new URLSearchParams();
|
|
276
|
+
if (args.type) params.set('resource_types', args.type);
|
|
277
|
+
if (args.page) params.set('page', args.page);
|
|
278
|
+
if (args.size) params.set('size', args.size);
|
|
279
|
+
const qs = params.toString();
|
|
280
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/resources${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
281
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
282
|
+
else {
|
|
283
|
+
const items = payload?.data?.items || [];
|
|
284
|
+
if (!items.length) { process.stderr.write('No resources found.\n'); }
|
|
285
|
+
else {
|
|
286
|
+
process.stdout.write(`Found ${payload.data.total || items.length} resource(s)\n\n`);
|
|
287
|
+
for (const r of items) process.stdout.write(formatResource(r));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
code = 0;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'resource': {
|
|
294
|
+
if (!shortId || !resourceId) { console.error('ERROR: short_id and resource_id are required'); break; }
|
|
295
|
+
spinnerId = startSpinner('Fetching resource');
|
|
296
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/resources/${resourceId}`, null, apiKey, apiBase, timeoutMs);
|
|
297
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
298
|
+
else { process.stdout.write(formatResource(payload?.data)); }
|
|
299
|
+
code = 0;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
case 'add-doc': {
|
|
303
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
304
|
+
if (!args.content) { console.error('ERROR: --content is required'); break; }
|
|
305
|
+
spinnerId = startSpinner('Creating document');
|
|
306
|
+
const body = { content: args.content };
|
|
307
|
+
if (args.title) body.title = args.title;
|
|
308
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/doc`, body, apiKey, apiBase, timeoutMs);
|
|
309
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
310
|
+
else { process.stdout.write('Document created!\n\n'); process.stdout.write(formatResource(payload?.data)); }
|
|
311
|
+
code = 0;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case 'add-urls': {
|
|
315
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
316
|
+
if (!args.urls) { console.error('ERROR: --urls is required'); break; }
|
|
317
|
+
const urls = args.urls.split(',').map(u => u.trim()).filter(Boolean);
|
|
318
|
+
if (urls.length > 10) { console.error('ERROR: maximum 10 URLs allowed'); break; }
|
|
319
|
+
spinnerId = startSpinner(`Adding ${urls.length} URL(s)`);
|
|
320
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/urls`, { urls }, apiKey, apiBase, timeoutMs);
|
|
321
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
322
|
+
else {
|
|
323
|
+
for (const r of (payload?.data || [])) {
|
|
324
|
+
const icon = r.status === 'success' ? '✓' : r.status === 'existed' ? '~' : '✗';
|
|
325
|
+
let line = `${icon} ${r.url} → ${r.status}`;
|
|
326
|
+
if (r.resource_id) line += ` (id: ${r.resource_id})`;
|
|
327
|
+
if (r.fail_reason) line += ` (${r.fail_reason})`;
|
|
328
|
+
process.stdout.write(line + '\n');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
code = 0;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'upload': {
|
|
335
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
336
|
+
if (!args.file) { console.error('ERROR: --file is required'); break; }
|
|
337
|
+
const endpoint = args.convert ? 'upload-doc' : 'upload';
|
|
338
|
+
spinnerId = startSpinner(`Uploading file (${endpoint})`);
|
|
339
|
+
const fileBuffer = await fs.readFile(args.file);
|
|
340
|
+
const blob = new Blob([fileBuffer]);
|
|
341
|
+
const formData = new FormData();
|
|
342
|
+
formData.append('file', blob, path.basename(args.file));
|
|
343
|
+
const payload = await uploadFormData(`/livedocs/${shortId}/resources/${endpoint}`, formData, apiKey, apiBase, timeoutMs);
|
|
344
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
345
|
+
else { process.stdout.write('File uploaded!\n\n'); process.stdout.write(formatResource(payload?.data)); }
|
|
346
|
+
code = 0;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'remove-resource': {
|
|
350
|
+
if (!shortId || !resourceId) { console.error('ERROR: short_id and resource_id are required'); break; }
|
|
351
|
+
spinnerId = startSpinner('Deleting resource');
|
|
352
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/resources/${resourceId}`, null, apiKey, apiBase, timeoutMs);
|
|
353
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
354
|
+
else { process.stdout.write(`Resource \`${resourceId}\` deleted.\n`); }
|
|
355
|
+
code = 0;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 'retrieve': {
|
|
359
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
360
|
+
if (!args.query) { console.error('ERROR: --query is required'); break; }
|
|
361
|
+
spinnerId = startSpinner('Retrieving from knowledge base');
|
|
362
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, { content: args.query }, apiKey, apiBase, timeoutMs);
|
|
363
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
364
|
+
else {
|
|
365
|
+
const results = payload?.data || [];
|
|
366
|
+
if (!results.length) { process.stderr.write('No results found.\n'); }
|
|
367
|
+
else {
|
|
368
|
+
process.stdout.write(`Found ${results.length} result(s)\n\n`);
|
|
369
|
+
for (const r of results) process.stdout.write(formatRetrieveResult(r));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
code = 0;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
console.error(`Unknown action: ${action}`);
|
|
377
|
+
usage();
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
process.stderr.write(`Error: ${err?.message || err}\n`);
|
|
381
|
+
} finally {
|
|
382
|
+
stopSpinner(spinnerId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process.exit(code);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
main().catch((err) => {
|
|
389
|
+
process.stderr.write(`Fatal: ${err?.message || err}\n`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Felo AI CLI - real-time search, PPT generation, web fetch,
|
|
3
|
+
"version": "0.2.14",
|
|
4
|
+
"description": "Felo AI CLI - real-time search, PPT generation, web fetch, YouTube subtitles, LiveDoc knowledge base from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"youtube-subtitles",
|
|
20
20
|
"x-search",
|
|
21
21
|
"twitter",
|
|
22
|
+
"livedoc",
|
|
23
|
+
"knowledge-base",
|
|
22
24
|
"cli",
|
|
23
25
|
"ai"
|
|
24
26
|
],
|