felo-ai 0.2.28 → 0.2.31

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,85 @@
1
+ {
2
+ "name": "felo-ai",
3
+ "owner": {
4
+ "name": "Felo-Inc",
5
+ "url": "https://github.com/Felo-Inc"
6
+ },
7
+ "metadata": {
8
+ "description": "Felo AI skills for Claude Code — real-time search, PPT generation, SuperAgent, LiveDoc knowledge base, web fetch, YouTube subtitles, and X (Twitter) search",
9
+ "version": "1.0.0"
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "felo-search",
14
+ "description": "Real-time web search powered by Felo AI",
15
+ "source": "./",
16
+ "strict": false,
17
+ "skills": [
18
+ "./felo-search"
19
+ ]
20
+ },
21
+ {
22
+ "name": "felo-livedoc",
23
+ "description": "Manage knowledge bases (LiveDocs) and semantic retrieval via Felo API",
24
+ "source": "./",
25
+ "strict": false,
26
+ "skills": [
27
+ "./felo-livedoc"
28
+ ]
29
+ },
30
+ {
31
+ "name": "felo-slides",
32
+ "description": "Generate PPT presentations from a prompt using Felo AI",
33
+ "source": "./",
34
+ "strict": false,
35
+ "skills": [
36
+ "./felo-slides"
37
+ ]
38
+ },
39
+ {
40
+ "name": "felo-superAgent",
41
+ "description": "SuperAgent conversation with SSE streaming and LiveDoc integration",
42
+ "source": "./",
43
+ "strict": false,
44
+ "skills": [
45
+ "./felo-superAgent"
46
+ ]
47
+ },
48
+ {
49
+ "name": "felo-web-fetch",
50
+ "description": "Fetch and extract webpage content in markdown, text, or HTML format",
51
+ "source": "./",
52
+ "strict": false,
53
+ "skills": [
54
+ "./felo-web-fetch"
55
+ ]
56
+ },
57
+ {
58
+ "name": "felo-youtube-subtitling",
59
+ "description": "Fetch YouTube video subtitles and captions by URL or video ID",
60
+ "source": "./",
61
+ "strict": false,
62
+ "skills": [
63
+ "./felo-youtube-subtitling"
64
+ ]
65
+ },
66
+ {
67
+ "name": "felo-x-search",
68
+ "description": "Search X (Twitter) tweets, users, and replies via Felo API",
69
+ "source": "./",
70
+ "strict": false,
71
+ "skills": [
72
+ "./felo-x-search"
73
+ ]
74
+ },
75
+ {
76
+ "name": "felo-content-to-slides",
77
+ "description": "Fetch content from a webpage or YouTube video and generate a PPT",
78
+ "source": "./",
79
+ "strict": false,
80
+ "skills": [
81
+ "./felo-content-to-slides"
82
+ ]
83
+ }
84
+ ]
85
+ }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.29] - 2026-03-18
9
+
10
+ ### Fixed
11
+
12
+ - Fix spinner animation displaying repeated lines in non-TTY environments (e.g. Claude Code TUI, pipes, CI) by checking `process.stderr.isTTY` before writing `\r` control characters; spinner is silently skipped when stderr is not a TTY
13
+
14
+ ---
15
+
8
16
  ## [0.2.25] - 2026-03-17
9
17
 
10
18
  ### Added
@@ -56,6 +56,7 @@ felo content-to-slides -v "https://www.youtube.com/watch?v=ID" [options]
56
56
  | `-l, --language <code>` | For --video: subtitle language (e.g. en, zh-CN) |
57
57
  | `-t, --timeout <seconds>` | Fetch timeout (default 60) |
58
58
  | `--poll-timeout <seconds>` | Max seconds to wait for PPT task (default 1200) |
59
+ | `--theme <id>` | PPT theme ID (list themes with `felo ppt-themes`) |
59
60
  | `-j, --json` | Output JSON with task_id and ppt/live_doc URL |
60
61
  | `--verbose` | Show polling status |
61
62
 
@@ -67,6 +68,9 @@ Provide **either** `--url` or `--video`, not both.
67
68
  # Web page → PPT (with readability)
68
69
  node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
69
70
 
71
+ # Web page → PPT with a specific theme
72
+ node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability --theme "THEME_ID"
73
+
70
74
  # YouTube → PPT, with extra instruction
71
75
  node src/cli.js content-to-slides -v "https://www.youtube.com/watch?v=xxx" --extra-prompt "max 10 slides"
72
76
 
@@ -11,6 +11,8 @@ Manage knowledge bases (LiveDocs) and their resources via the Felo API.
11
11
  - Semantic retrieval across knowledge base resources
12
12
  - Route relevant resources by query for targeted retrieval
13
13
  - Full CRUD for resources within a LiveDoc
14
+ - Manage README content for each LiveDoc
15
+ - Task management: create, update, delete tasks with comments and change history
14
16
 
15
17
  **When to use:**
16
18
  - Building or managing a knowledge base
@@ -87,8 +89,19 @@ felo livedoc route SHORT_ID --query "latest AI research" --max-resources 5
87
89
  | `add-urls <short_id>` | Add URL resources (max 10) |
88
90
  | `upload <short_id>` | Upload a file resource |
89
91
  | `remove-resource <short_id> <resource_id>` | Delete a resource |
92
+ | `update-resource <short_id> <resource_id>` | Update resource title, snippet, or thumbnail |
90
93
  | `retrieve <short_id>` | Semantic retrieval (auto-routes if no `--resource-ids`) |
91
94
  | `route <short_id>` | Route relevant resource IDs by query |
95
+ | `get-readme <short_id>` | Get README content |
96
+ | `update-readme <short_id>` | Create or replace README |
97
+ | `append-readme <short_id>` | Append content to README |
98
+ | `delete-readme <short_id>` | Delete README |
99
+ | `tasks <short_id>` | List tasks (filter by `--status`, `--labels`) |
100
+ | `create-task <short_id>` | Create a task |
101
+ | `update-task <short_id> <task_id>` | Partially update a task |
102
+ | `delete-task <short_id> <task_id>` | Delete a task |
103
+ | `task-records <short_id> <task_id>` | List task records (comments + change history) |
104
+ | `add-task-comment <short_id> <task_id>` | Add a comment to a task |
92
105
 
93
106
  ---
94
107
 
@@ -14,10 +14,12 @@ Trigger this skill when users want to:
14
14
  - **Semantic retrieval:** Search across knowledge base resources using natural language queries
15
15
  - **Route resources:** Find relevant resource IDs by query for targeted retrieval
16
16
  - **Resource management:** List, view, or delete resources within a LiveDoc
17
+ - **README management:** Get, create, update, append, or delete a LiveDoc's README
18
+ - **Task management:** Create, update, delete tasks; add comments; view change history
17
19
 
18
20
  **Trigger words:**
19
- - English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval, route resources
20
- - 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索, 知识检索
21
+ - English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval, route resources, readme, task, task management, comment
22
+ - 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索, 知识检索, 任务, 任务管理, 评论
21
23
 
22
24
  **Explicit commands:** `/felo-livedoc`, "livedoc", "felo livedoc"
23
25
 
@@ -111,6 +113,12 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs resource SHORT_ID RES
111
113
  node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs remove-resource SHORT_ID RESOURCE_ID
112
114
  ```
113
115
 
116
+ **Update a resource (title/snippet/thumbnail):**
117
+ ```bash
118
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-resource SHORT_ID RESOURCE_ID --title "New Title"
119
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-resource SHORT_ID RESOURCE_ID --snippet "New summary" --thumbnail "https://example.com/thumb.png"
120
+ ```
121
+
114
122
  ### Semantic Retrieval
115
123
 
116
124
  **Route relevant resources by query:**
@@ -147,6 +155,67 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs content SHORT_ID RESO
147
155
  node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information"
148
156
  node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information" --max-chunk 5
149
157
  ```
158
+
159
+ ### README Management
160
+
161
+ **Get README:**
162
+ ```bash
163
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs get-readme SHORT_ID
164
+ ```
165
+
166
+ **Create or replace README:**
167
+ ```bash
168
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-readme SHORT_ID --content "# My KB\n\nThis is the README."
169
+ ```
170
+
171
+ **Append to README:**
172
+ ```bash
173
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs append-readme SHORT_ID --content "\n\n## New Section\n\nAdditional content."
174
+ ```
175
+
176
+ **Delete README:**
177
+ ```bash
178
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs delete-readme SHORT_ID
179
+ ```
180
+
181
+ ### Task Management
182
+
183
+ **List tasks (with optional filters):**
184
+ ```bash
185
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID
186
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID --status 0
187
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID --labels "docs,priority-high"
188
+ ```
189
+
190
+ **Create a task:**
191
+ ```bash
192
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs create-task SHORT_ID --title "Write docs" --status 0 --sort 0
193
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs create-task SHORT_ID --title "Write docs" --status 0 --sort 0 --description "API docs" --labels "docs"
194
+ ```
195
+
196
+ Task status values: `0`=TODO, `1`=IN_PROGRESS, `2`=DONE
197
+
198
+ **Update a task (partial update):**
199
+ ```bash
200
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-task SHORT_ID TASK_ID --status 1
201
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-task SHORT_ID TASK_ID --title "New title" --labels "docs,done"
202
+ ```
203
+
204
+ **Delete a task:**
205
+ ```bash
206
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs delete-task SHORT_ID TASK_ID
207
+ ```
208
+
209
+ **List task records (comments + change history):**
210
+ ```bash
211
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs task-records SHORT_ID TASK_ID
212
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs task-records SHORT_ID TASK_ID --record-type comment
213
+ ```
214
+
215
+ **Add a comment to a task:**
216
+ ```bash
217
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-task-comment SHORT_ID TASK_ID --content "This is a comment."
218
+ ```
150
219
  ### Options
151
220
 
152
221
  All commands support:
@@ -194,6 +263,16 @@ The API returns JSON with this structure:
194
263
  - `LIVEDOC_RESOURCE_UPLOAD_FAILED` — File upload failed
195
264
  - `LIVEDOC_RESOURCE_ADD_URLS_FAILED` — URL addition failed
196
265
  - `LIVEDOC_RESOURCE_RETRIEVE_FAILED` — Semantic retrieval failed
266
+ - `LIVEDOC_README_GET_FAILED` — Failed to get README
267
+ - `LIVEDOC_README_UPDATE_FAILED` — Failed to create or update README
268
+ - `LIVEDOC_README_DELETE_FAILED` — Failed to delete README
269
+ - `LIVEDOC_TASK_LIST_FAILED` — Failed to list tasks
270
+ - `LIVEDOC_TASK_CREATE_FAILED` — Failed to create task
271
+ - `LIVEDOC_TASK_UPDATE_FAILED` — Failed to update task
272
+ - `LIVEDOC_TASK_DELETE_FAILED` — Failed to delete task
273
+ - `LIVEDOC_TASK_NOT_FOUND` — Task does not exist
274
+ - `LIVEDOC_TASK_RECORD_LIST_FAILED` — Failed to list task records
275
+ - `LIVEDOC_TASK_COMMENT_CREATE_FAILED` — Failed to add comment
197
276
 
198
277
  ### Missing API Key
199
278
 
@@ -12,6 +12,7 @@ const SPINNER_INTERVAL_MS = 80;
12
12
  const STATUS_PAD = 56;
13
13
 
14
14
  function startSpinner(message) {
15
+ if (!process.stderr.isTTY) return null;
15
16
  const start = Date.now();
16
17
  let i = 0;
17
18
  const id = setInterval(() => {
@@ -25,7 +26,7 @@ function startSpinner(message) {
25
26
 
26
27
  function stopSpinner(id) {
27
28
  if (id != null) clearInterval(id);
28
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
30
  }
30
31
 
31
32
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
@@ -126,6 +127,28 @@ function formatRetrieveResult(r) {
126
127
  return out;
127
128
  }
128
129
 
130
+ function formatTask(t) {
131
+ if (!t) return '';
132
+ let out = `### ${t.title || '(untitled)'}\n`;
133
+ out += `- Task ID: \`${t.id}\`\n`;
134
+ out += `- Status: ${t.status === 0 ? 'TODO' : t.status === 1 ? 'IN_PROGRESS' : t.status === 2 ? 'DONE' : t.status}\n`;
135
+ if (t.sort != null) out += `- Sort: ${t.sort}\n`;
136
+ if (t.description) out += `- Description: ${t.description}\n`;
137
+ if (t.labels?.length) out += `- Labels: ${t.labels.join(', ')}\n`;
138
+ if (t.created_at) out += `- Created: ${t.created_at}\n`;
139
+ out += '\n';
140
+ return out;
141
+ }
142
+
143
+ function formatTaskRecord(r) {
144
+ if (!r) return '';
145
+ let out = `- [${r.record_type}] `;
146
+ if (r.content) out += r.content;
147
+ else if (r.meta) out += JSON.stringify(r.meta);
148
+ out += ` (id: ${r.id}, ${r.created_at || ''})\n`;
149
+ return out;
150
+ }
151
+
129
152
  // ── CLI ──
130
153
 
131
154
  function usage() {
@@ -143,11 +166,22 @@ function usage() {
143
166
  ' add-urls <short_id> Add URLs (--urls required, comma-separated, max 10)',
144
167
  ' upload <short_id> Upload file (--file required, --convert optional)',
145
168
  ' remove-resource <short_id> <resource_id> Delete a resource',
169
+ ' update-resource <short_id> <resource_id> Update resource title/snippet/thumbnail',
146
170
  ' retrieve <short_id> Semantic search (--query required, --resource-ids optional)',
147
171
  ' route <short_id> Route relevant resources by query (--query required)',
148
172
  ' download <short_id> <resource_id> Download source file to disk',
149
173
  ' content <short_id> <resource_id> Get text content of a resource',
150
174
  ' ppt-retrieve <short_id> PPT page deep retrieval (--resource-id, --page-number, --query required)',
175
+ ' get-readme <short_id> Get README content',
176
+ ' update-readme <short_id> Create or replace README (--content required)',
177
+ ' append-readme <short_id> Append to README (--content required)',
178
+ ' delete-readme <short_id> Delete README',
179
+ ' tasks <short_id> List tasks (--status, --labels optional)',
180
+ ' create-task <short_id> Create a task (--title, --status, --sort required)',
181
+ ' update-task <short_id> <task_id> Partially update a task',
182
+ ' delete-task <short_id> <task_id> Delete a task',
183
+ ' task-records <short_id> <task_id> List task records (comments + history)',
184
+ ' add-task-comment <short_id> <task_id> Add a comment (--content required)',
151
185
  '',
152
186
  'Options:',
153
187
  ' --name <name> LiveDoc name',
@@ -157,8 +191,8 @@ function usage() {
157
191
  ' --page <n> Page number',
158
192
  ' --size <n> Page size',
159
193
  ' --type <type> Resource type filter',
160
- ' --content <text> Document content',
161
- ' --title <title> Document title',
194
+ ' --content <text> Document/README/comment content',
195
+ ' --title <title> Document/task title',
162
196
  ' --urls <urls> Comma-separated URLs',
163
197
  ' --file <path> File path to upload',
164
198
  ' --convert Convert uploaded file to document',
@@ -170,6 +204,10 @@ function usage() {
170
204
  ' --max-chunk <n> Max chunks to return (ppt-retrieve, default 3)',
171
205
  ' --expires-in <s> Presigned URL expiry in seconds (download, default 3600)',
172
206
  ' --output <path> Output file path (download, default: filename from response)',
207
+ ' --status <n> Task status: 0=TODO, 1=IN_PROGRESS, 2=DONE',
208
+ ' --sort <n> Task sort order (non-negative integer)',
209
+ ' --labels <labels> Comma-separated labels (tasks)',
210
+ ' --record-type <type> Record type filter: comment, edit, status_change',
173
211
  ' -j, --json Output raw JSON',
174
212
  ' -t, --timeout <ms> Timeout in ms (default: 60000)',
175
213
  ' --help Show this help',
@@ -180,7 +218,8 @@ function parseArgs(argv) {
180
218
  action: '', positional: [], name: '', description: '', icon: '',
181
219
  keyword: '', page: '', size: '', type: '', content: '', title: '',
182
220
  urls: '', file: '', convert: false, query: '', resourceIds: '', maxResources: '',
183
- resourceId: '', pageNumber: '', maxChunk: '', expiresIn: '',
221
+ resourceId: '', pageNumber: '', maxChunk: '', expiresIn: '', output: '',
222
+ status: '', sort: '', labels: '', recordType: '',
184
223
  json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
185
224
  };
186
225
  const positional = [];
@@ -207,6 +246,11 @@ function parseArgs(argv) {
207
246
  else if (a === '--page-number') out.pageNumber = argv[++i] || '';
208
247
  else if (a === '--max-chunk') out.maxChunk = argv[++i] || '';
209
248
  else if (a === '--expires-in') out.expiresIn = argv[++i] || '';
249
+ else if (a === '--output') out.output = argv[++i] || '';
250
+ else if (a === '--status') out.status = argv[++i] || '';
251
+ else if (a === '--sort') out.sort = argv[++i] || '';
252
+ else if (a === '--labels') out.labels = argv[++i] || '';
253
+ else if (a === '--record-type') out.recordType = argv[++i] || '';
210
254
  else if (a === '-t' || a === '--timeout') {
211
255
  const n = parseInt(argv[++i] || '', 10);
212
256
  if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
@@ -373,6 +417,19 @@ async function main() {
373
417
  code = 0;
374
418
  break;
375
419
  }
420
+ case 'update-resource': {
421
+ if (!shortId || !resourceId) { console.error('ERROR: short_id and resource_id are required'); break; }
422
+ spinnerId = startSpinner('Updating resource');
423
+ const body = {};
424
+ if (args.title !== undefined) body.title = args.title;
425
+ if (args.snippet !== undefined) body.snippet = args.snippet;
426
+ if (args.thumbnail !== undefined) body.thumbnail = args.thumbnail;
427
+ const payload = await apiRequest('PUT', `/livedocs/${shortId}/resources/${resourceId}`, body, apiKey, apiBase, timeoutMs);
428
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
429
+ else { process.stdout.write('Resource updated!\n\n'); process.stdout.write(formatResource(payload?.data)); }
430
+ code = 0;
431
+ break;
432
+ }
376
433
  case 'retrieve': {
377
434
  if (!shortId) { console.error('ERROR: short_id is required'); break; }
378
435
  if (!args.query) { console.error('ERROR: --query is required'); break; }
@@ -489,6 +546,140 @@ async function main() {
489
546
  code = 0;
490
547
  break;
491
548
  }
549
+ case 'get-readme': {
550
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
551
+ spinnerId = startSpinner('Fetching README');
552
+ const payload = await apiRequest('GET', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
553
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
554
+ else { process.stdout.write(payload?.data?.content || '(empty)\n'); }
555
+ code = 0;
556
+ break;
557
+ }
558
+ case 'update-readme': {
559
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
560
+ if (args.content === undefined || args.content === '') { console.error('ERROR: --content is required'); break; }
561
+ spinnerId = startSpinner('Updating README');
562
+ await apiRequest('PUT', `/livedocs/${shortId}/readme`, { content: args.content }, apiKey, apiBase, timeoutMs);
563
+ if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
564
+ else { process.stdout.write('README updated.\n'); }
565
+ code = 0;
566
+ break;
567
+ }
568
+ case 'append-readme': {
569
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
570
+ if (!args.content) { console.error('ERROR: --content is required'); break; }
571
+ spinnerId = startSpinner('Appending to README');
572
+ await apiRequest('POST', `/livedocs/${shortId}/readme/append`, { content: args.content }, apiKey, apiBase, timeoutMs);
573
+ if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
574
+ else { process.stdout.write('README appended.\n'); }
575
+ code = 0;
576
+ break;
577
+ }
578
+ case 'delete-readme': {
579
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
580
+ spinnerId = startSpinner('Deleting README');
581
+ await apiRequest('DELETE', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
582
+ if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
583
+ else { process.stdout.write('README deleted.\n'); }
584
+ code = 0;
585
+ break;
586
+ }
587
+ case 'tasks': {
588
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
589
+ spinnerId = startSpinner('Listing tasks');
590
+ const params = new URLSearchParams();
591
+ if (args.status !== '') params.set('status', args.status);
592
+ if (args.labels) args.labels.split(',').map(l => l.trim()).filter(Boolean).forEach(l => params.append('labels', l));
593
+ if (args.page) params.set('page', args.page);
594
+ if (args.size) params.set('size', args.size);
595
+ const qs = params.toString();
596
+ const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
597
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
598
+ else {
599
+ const items = payload?.data?.items || [];
600
+ if (!items.length) { process.stderr.write('No tasks found.\n'); }
601
+ else {
602
+ process.stdout.write(`Found ${payload.data.total || items.length} task(s)\n\n`);
603
+ for (const t of items) process.stdout.write(formatTask(t));
604
+ }
605
+ }
606
+ code = 0;
607
+ break;
608
+ }
609
+ case 'create-task': {
610
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
611
+ if (!args.title) { console.error('ERROR: --title is required'); break; }
612
+ if (args.status === '') { console.error('ERROR: --status is required'); break; }
613
+ if (args.sort === '') { console.error('ERROR: --sort is required'); break; }
614
+ spinnerId = startSpinner('Creating task');
615
+ const body = { title: args.title, status: parseInt(args.status, 10), sort: parseInt(args.sort, 10) };
616
+ if (args.description) body.description = args.description;
617
+ if (args.labels) body.labels = args.labels.split(',').map(l => l.trim()).filter(Boolean);
618
+ const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks`, body, apiKey, apiBase, timeoutMs);
619
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
620
+ else { process.stdout.write('Task created!\n\n'); process.stdout.write(formatTask(payload?.data)); }
621
+ code = 0;
622
+ break;
623
+ }
624
+ case 'update-task': {
625
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
626
+ if (!resourceId) { console.error('ERROR: task_id is required'); break; }
627
+ spinnerId = startSpinner('Updating task');
628
+ const body = {};
629
+ if (args.title) body.title = args.title;
630
+ if (args.description !== undefined) body.description = args.description;
631
+ if (args.status !== '') body.status = parseInt(args.status, 10);
632
+ if (args.sort !== '') body.sort = parseInt(args.sort, 10);
633
+ if (args.labels !== undefined) body.labels = args.labels.split(',').map(l => l.trim()).filter(Boolean);
634
+ const payload = await apiRequest('PATCH', `/livedocs/${shortId}/tasks/${resourceId}`, body, apiKey, apiBase, timeoutMs);
635
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
636
+ else { process.stdout.write('Task updated!\n\n'); process.stdout.write(formatTask(payload?.data)); }
637
+ code = 0;
638
+ break;
639
+ }
640
+ case 'delete-task': {
641
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
642
+ if (!resourceId) { console.error('ERROR: task_id is required'); break; }
643
+ spinnerId = startSpinner('Deleting task');
644
+ await apiRequest('DELETE', `/livedocs/${shortId}/tasks/${resourceId}`, null, apiKey, apiBase, timeoutMs);
645
+ if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
646
+ else { process.stdout.write(`Task \`${resourceId}\` deleted.\n`); }
647
+ code = 0;
648
+ break;
649
+ }
650
+ case 'task-records': {
651
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
652
+ if (!resourceId) { console.error('ERROR: task_id is required'); break; }
653
+ spinnerId = startSpinner('Fetching task records');
654
+ const params = new URLSearchParams();
655
+ if (args.recordType) params.set('record_type', args.recordType);
656
+ if (args.page) params.set('page', args.page);
657
+ if (args.size) params.set('size', args.size);
658
+ const qs = params.toString();
659
+ const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks/${resourceId}/records${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
660
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
661
+ else {
662
+ const items = payload?.data?.items || [];
663
+ if (!items.length) { process.stderr.write('No records found.\n'); }
664
+ else {
665
+ process.stdout.write(`Found ${payload.data.total || items.length} record(s)\n\n`);
666
+ for (const r of items) process.stdout.write(formatTaskRecord(r));
667
+ }
668
+ }
669
+ code = 0;
670
+ break;
671
+ }
672
+ case 'add-task-comment': {
673
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
674
+ if (!resourceId) { console.error('ERROR: task_id is required'); break; }
675
+ if (!args.content) { console.error('ERROR: --content is required'); break; }
676
+ spinnerId = startSpinner('Adding comment');
677
+ const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks/${resourceId}/comments`, { content: args.content }, apiKey, apiBase, timeoutMs);
678
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
679
+ else { process.stdout.write('Comment added.\n'); process.stdout.write(formatTaskRecord(payload?.data)); }
680
+ code = 0;
681
+ break;
682
+ }
492
683
  default:
493
684
  console.error(`Unknown action: ${action}`);
494
685
  usage();
@@ -75,9 +75,22 @@ node felo-slides/scripts/run_ppt_task.mjs \
75
75
  --timeout 60
76
76
  ```
77
77
 
78
+ To apply a specific theme, first list available themes with `felo ppt-themes`, then pass the theme ID:
79
+
80
+ ```bash
81
+ node felo-slides/scripts/run_ppt_task.mjs \
82
+ --query "USER_PROMPT_HERE" \
83
+ --theme "THEME_ID_HERE" \
84
+ --interval 10 \
85
+ --max-wait 1800 \
86
+ --timeout 60
87
+ ```
88
+
78
89
  Script behavior:
79
90
 
80
91
  - Creates task via `POST https://openapi.felo.ai/v2/ppts`
92
+ - Supports optional `--theme <id>` to apply a PPT theme (sends `ppt_config.ai_theme_id`)
93
+ - Supports optional `--task-id <id>` to resume polling an existing task (skips creation)
81
94
  - Polls via `GET https://openapi.felo.ai/v2/tasks/{task_id}/historical`
82
95
  - Treats `COMPLETED`/`SUCCESS` as success terminal (case-insensitive)
83
96
  - Treats `FAILED`/`ERROR` as failure terminal
@@ -152,6 +165,14 @@ Timeout handling:
152
165
 
153
166
  - If timeout reached, return last known status and instruct user to retry later
154
167
  - Include `task_id` so user can query again
168
+ - **IMPORTANT**: To resume a timed-out task, use `--task-id` instead of `--query` to avoid creating a duplicate PPT:
169
+
170
+ ```bash
171
+ node felo-slides/scripts/run_ppt_task.mjs \
172
+ --task-id "TASK_ID_HERE" \
173
+ --interval 10 \
174
+ --max-wait 1800
175
+ ```
155
176
 
156
177
  ## Important Notes
157
178
 
@@ -12,7 +12,9 @@ function usage() {
12
12
  ' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
13
13
  '',
14
14
  'Options:',
15
- ' --query <text> PPT prompt (required)',
15
+ ' --query <text> PPT prompt (required unless --task-id is given)',
16
+ ' --task-id <id> Resume polling an existing task (skip creation)',
17
+ ' --theme <id> PPT theme ID (from ppt-themes)',
16
18
  ' --interval <seconds> Poll interval, default 10',
17
19
  ' --max-wait <seconds> Max wait time, default 1800',
18
20
  ' --timeout <seconds> Request timeout, default 60',
@@ -26,6 +28,8 @@ function usage() {
26
28
  function parseArgs(argv) {
27
29
  const out = {
28
30
  query: '',
31
+ taskId: '',
32
+ theme: '',
29
33
  intervalSec: DEFAULT_INTERVAL_SEC,
30
34
  maxWaitSec: DEFAULT_MAX_WAIT_SEC,
31
35
  timeoutSec: DEFAULT_TIMEOUT_SEC,
@@ -44,6 +48,12 @@ function parseArgs(argv) {
44
48
  } else if (a === '--query') {
45
49
  out.query = argv[i + 1] ?? '';
46
50
  i += 1;
51
+ } else if (a === '--task-id') {
52
+ out.taskId = argv[i + 1] ?? '';
53
+ i += 1;
54
+ } else if (a === '--theme') {
55
+ out.theme = argv[i + 1] ?? '';
56
+ i += 1;
47
57
  } else if (a === '--interval') {
48
58
  out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
49
59
  i += 1;
@@ -128,7 +138,11 @@ function extractTaskUrls(historicalData, createData) {
128
138
  };
129
139
  }
130
140
 
131
- async function createTask(apiKey, apiBase, query, timeoutMs) {
141
+ async function createTask(apiKey, apiBase, query, timeoutMs, theme) {
142
+ const reqBody = { query };
143
+ if (theme) {
144
+ reqBody.ppt_config = { ai_theme_id: theme };
145
+ }
132
146
  const payload = await fetchJson(
133
147
  `${apiBase}/v2/ppts`,
134
148
  {
@@ -138,7 +152,7 @@ async function createTask(apiKey, apiBase, query, timeoutMs) {
138
152
  Authorization: `Bearer ${apiKey}`,
139
153
  'Content-Type': 'application/json',
140
154
  },
141
- body: JSON.stringify({ query }),
155
+ body: JSON.stringify(reqBody),
142
156
  },
143
157
  timeoutMs
144
158
  );
@@ -170,7 +184,7 @@ async function main() {
170
184
  usage();
171
185
  process.exit(0);
172
186
  }
173
- if (!args.query) {
187
+ if (!args.query && !args.taskId) {
174
188
  usage();
175
189
  process.exit(1);
176
190
  }
@@ -186,10 +200,22 @@ async function main() {
186
200
  const intervalMs = args.intervalSec * 1000;
187
201
  const maxWaitMs = args.maxWaitSec * 1000;
188
202
 
189
- const createData = await createTask(apiKey, apiBase, args.query, timeoutMs);
190
- const taskId = createData.task_id;
191
- if (args.verbose) {
192
- console.error(`Task ID: ${taskId}`);
203
+ let createData = {};
204
+ let taskId;
205
+
206
+ if (args.taskId) {
207
+ // Resume polling an existing task
208
+ taskId = args.taskId;
209
+ if (args.verbose) {
210
+ console.error(`Resuming task: ${taskId}`);
211
+ }
212
+ } else {
213
+ // Create a new task
214
+ createData = await createTask(apiKey, apiBase, args.query, timeoutMs, args.theme);
215
+ taskId = createData.task_id;
216
+ if (args.verbose) {
217
+ console.error(`Task ID: ${taskId}`);
218
+ }
193
219
  }
194
220
 
195
221
  const startAt = Date.now();