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.
@@ -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.12",
4
- "description": "Felo AI CLI - real-time search, PPT generation, web fetch, and YouTube subtitles from the terminal",
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
  ],