felo-ai 0.2.1 → 0.2.3

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.
@@ -6,7 +6,7 @@ Generate presentation slides with the Felo PPT Task API (asynchronous workflow).
6
6
 
7
7
  - Generate a PPT deck from a single prompt
8
8
  - Poll task status automatically until completion/failure/timeout
9
- - Return `live_doc_url` immediately when the task is completed
9
+ - Return `ppt_url` immediately when the task is completed (fallback to `live_doc_url`)
10
10
  - Return `task_id` for follow-up tracking
11
11
 
12
12
  ## Quick Start
@@ -54,12 +54,12 @@ Based on Felo v2 PPT Task API:
54
54
  2. Query status (optional): `GET /v2/tasks/{task_id}/status`
55
55
  3. Query historical/result: `GET /v2/tasks/{task_id}/historical`
56
56
 
57
- The skill polls every 10 seconds (max wait 600 seconds). It stops immediately on `COMPLETED`/`SUCCESS` and returns `live_doc_url`.
57
+ The skill polls every 10 seconds (max wait 1800 seconds). It stops immediately on `COMPLETED`/`SUCCESS` and returns `ppt_url` (fallback `live_doc_url`).
58
58
 
59
59
  Internal script example:
60
60
 
61
61
  ```bash
62
- node felo-slides/scripts/run_ppt_task.mjs --query "Felo product intro, 3 slides" --interval 10 --max-wait 600
62
+ node felo-slides/scripts/run_ppt_task.mjs --query "Felo product intro, 3 slides" --interval 10 --max-wait 1800
63
63
  ```
64
64
 
65
65
  ## Troubleshooting
@@ -76,7 +76,7 @@ The key is invalid or revoked. Generate a new key from [felo.ai](https://felo.ai
76
76
 
77
77
  The task may still be processing. Retry later with the same context, or run the script with `--verbose`.
78
78
 
79
- ### Task completed but no `live_doc_url`
79
+ ### Task completed but no `ppt_url` / `live_doc_url`
80
80
 
81
81
  Use the returned `task_id` to query historical endpoint again.
82
82
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: felo-slides
3
- description: "Generate PPT/slides with Felo PPT Task API in Claude Code. Use when users ask to create/make/generate/export presentations or slide decks, or when explicit commands like /felo-slides are used. Handles API key check, task creation, polling, and final live_doc_url output."
3
+ description: "Generate PPT/slides with Felo PPT Task API in Claude Code. Use when users ask to create/make/generate/export presentations or slide decks, or when explicit commands like /felo-slides are used. Handles API key check, task creation, polling, and final ppt_url output."
4
4
  ---
5
5
 
6
6
  # Felo Slides Skill
@@ -67,7 +67,7 @@ Use the bundled script (no `jq` dependency):
67
67
  node felo-slides/scripts/run_ppt_task.mjs \
68
68
  --query "USER_PROMPT_HERE" \
69
69
  --interval 10 \
70
- --max-wait 600 \
70
+ --max-wait 1800 \
71
71
  --timeout 60
72
72
  ```
73
73
 
@@ -77,7 +77,7 @@ Script behavior:
77
77
  - Treats `COMPLETED`/`SUCCESS` as success terminal (case-insensitive)
78
78
  - Treats `FAILED`/`ERROR` as failure terminal
79
79
  - Stops polling immediately on terminal status
80
- - Prints `live_doc_url` on success
80
+ - Prints `ppt_url` on success (fallback: `live_doc_url`)
81
81
 
82
82
  Optional debug output:
83
83
 
@@ -85,7 +85,7 @@ Optional debug output:
85
85
  node felo-slides/scripts/run_ppt_task.mjs \
86
86
  --query "USER_PROMPT_HERE" \
87
87
  --interval 10 \
88
- --max-wait 600 \
88
+ --max-wait 1800 \
89
89
  --json \
90
90
  --verbose
91
91
  ```
@@ -93,6 +93,7 @@ node felo-slides/scripts/run_ppt_task.mjs \
93
93
  This outputs structured JSON including:
94
94
  - `task_id`
95
95
  - `task_status`
96
+ - `ppt_url`
96
97
  - `live_doc_url`
97
98
  - `livedoc_short_id`
98
99
  - `ppt_business_id`
@@ -100,7 +101,7 @@ This outputs structured JSON including:
100
101
  ### Step 4: Return structured result
101
102
 
102
103
  On success, return:
103
- - `live_doc_url` immediately (script default output)
104
+ - `ppt_url` immediately (script default output, fallback `live_doc_url`)
104
105
  - if `--json` is used, also include `task_id`, terminal status, and optional metadata
105
106
 
106
107
  ## Output Format
@@ -111,7 +112,8 @@ Use this response structure:
111
112
  ## PPT Generation Result
112
113
  - Task ID: <task_id>
113
114
  - Status: <status>
114
- - Live Doc URL: <live_doc_url>
115
+ - PPT URL: <ppt_url>
116
+ - Live Doc URL: <live_doc_url or N/A>
115
117
 
116
118
  ## Notes
117
119
  - livedoc_short_id: <value or N/A>
@@ -2,7 +2,7 @@
2
2
 
3
3
  const DEFAULT_API_BASE = 'https://openapi.felo.ai';
4
4
  const DEFAULT_INTERVAL_SEC = 10;
5
- const DEFAULT_MAX_WAIT_SEC = 600;
5
+ const DEFAULT_MAX_WAIT_SEC = 1800;
6
6
  const DEFAULT_TIMEOUT_SEC = 60;
7
7
 
8
8
  function usage() {
@@ -14,7 +14,7 @@ function usage() {
14
14
  'Options:',
15
15
  ' --query <text> PPT prompt (required)',
16
16
  ' --interval <seconds> Poll interval, default 10',
17
- ' --max-wait <seconds> Max wait time, default 600',
17
+ ' --max-wait <seconds> Max wait time, default 1800',
18
18
  ' --timeout <seconds> Request timeout, default 60',
19
19
  ' --json Print JSON output',
20
20
  ' --verbose Print polling status to stderr',
@@ -114,12 +114,18 @@ async function fetchJson(url, init, timeoutMs) {
114
114
  }
115
115
  }
116
116
 
117
- function extractLiveDocUrl(historicalData, createData) {
118
- const url = historicalData?.live_doc_url;
119
- if (url) return url;
120
- const sid = historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id;
121
- if (sid) return `https://felo.ai/livedoc/${sid}`;
122
- return '';
117
+ function extractTaskUrls(historicalData, createData) {
118
+ const pptUrl = historicalData?.ppt_url || '';
119
+ const liveDocUrl =
120
+ historicalData?.live_doc_url ||
121
+ (historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id
122
+ ? `https://felo.ai/livedoc/${historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id}`
123
+ : '');
124
+ return {
125
+ pptUrl,
126
+ liveDocUrl,
127
+ displayUrl: pptUrl || liveDocUrl,
128
+ };
123
129
  }
124
130
 
125
131
  async function createTask(apiKey, apiBase, query, timeoutMs) {
@@ -192,7 +198,7 @@ async function main() {
192
198
  while (Date.now() - startAt <= maxWaitMs) {
193
199
  const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
194
200
  const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
195
- const liveDocUrl = extractLiveDocUrl(historicalData, createData);
201
+ const urls = extractTaskUrls(historicalData, createData);
196
202
  lastStatus = taskStatus || 'UNKNOWN';
197
203
 
198
204
  if (args.verbose) {
@@ -201,8 +207,8 @@ async function main() {
201
207
  }
202
208
 
203
209
  if (taskStatus === 'COMPLETED' || taskStatus === 'SUCCESS') {
204
- if (!liveDocUrl) {
205
- throw new Error('Task completed but live_doc_url is missing');
210
+ if (!urls.displayUrl) {
211
+ throw new Error('Task completed but no ppt_url/live_doc_url is available');
206
212
  }
207
213
  if (args.json) {
208
214
  console.log(
@@ -212,7 +218,8 @@ async function main() {
212
218
  data: {
213
219
  task_id: taskId,
214
220
  task_status: taskStatus,
215
- live_doc_url: liveDocUrl,
221
+ ppt_url: urls.pptUrl || null,
222
+ live_doc_url: urls.liveDocUrl || null,
216
223
  livedoc_short_id: createData.livedoc_short_id ?? historicalData.live_doc_short_id ?? historicalData.livedoc_short_id ?? null,
217
224
  ppt_business_id: createData.ppt_business_id ?? null,
218
225
  },
@@ -222,7 +229,7 @@ async function main() {
222
229
  )
223
230
  );
224
231
  } else {
225
- console.log(liveDocUrl);
232
+ console.log(urls.displayUrl);
226
233
  }
227
234
  return;
228
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Felo AI CLI - real-time search from the terminal",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
package/src/cli.js CHANGED
@@ -9,6 +9,31 @@ import * as config from './config.js';
9
9
  const require = createRequire(import.meta.url);
10
10
  const pkg = require('../package.json');
11
11
 
12
+ /** Delay (ms) before process.exit to let Windows libuv finish handle cleanup. */
13
+ const EXIT_DELAY_MS = 50;
14
+
15
+ /**
16
+ * Flush stdout then stderr, then exit after a short delay. Avoids Node.js
17
+ * Windows UV_HANDLE_CLOSING assertion when process.exit() runs while streams
18
+ * or other handles are still closing.
19
+ * @param {number} code - Exit code.
20
+ */
21
+ function flushStdioThenExit(code) {
22
+ const doExit = () => setTimeout(() => process.exit(code), EXIT_DELAY_MS);
23
+ const flushStderr = () => {
24
+ if (process.stderr?.writable && !process.stderr.destroyed) {
25
+ process.stderr.write('', () => doExit());
26
+ } else {
27
+ doExit();
28
+ }
29
+ };
30
+ if (process.stdout?.writable && !process.stdout.destroyed) {
31
+ process.stdout.write('', () => flushStderr());
32
+ } else {
33
+ flushStderr();
34
+ }
35
+ }
36
+
12
37
  const program = new Command();
13
38
 
14
39
  program
@@ -31,8 +56,7 @@ program
31
56
  timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
32
57
  });
33
58
  process.exitCode = code;
34
- // Defer exit so stdout/stderr can flush; avoids Node.js Windows UV_HANDLE_CLOSING assertion
35
- setTimeout(() => process.exit(code), 0);
59
+ flushStdioThenExit(code);
36
60
  });
37
61
 
38
62
  program
@@ -53,8 +77,7 @@ program
53
77
  pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
54
78
  });
55
79
  process.exitCode = code;
56
- // Defer exit so stderr can flush; reduces Node.js Windows assertion (UV_HANDLE_CLOSING)
57
- setTimeout(() => process.exit(code), 0);
80
+ flushStdioThenExit(code);
58
81
  });
59
82
 
60
83
  const configCmd = program
@@ -68,9 +91,10 @@ configCmd
68
91
  try {
69
92
  await config.setConfig(key, value);
70
93
  console.log(`Set ${key}`);
94
+ flushStdioThenExit(0);
71
95
  } catch (e) {
72
96
  console.error('Error:', e.message);
73
- process.exit(1);
97
+ flushStdioThenExit(1);
74
98
  }
75
99
  });
76
100
 
@@ -85,9 +109,10 @@ configCmd
85
109
  } else {
86
110
  console.log(config.maskValueForDisplay(key, value));
87
111
  }
112
+ flushStdioThenExit(0);
88
113
  } catch (e) {
89
114
  console.error('Error:', e.message);
90
- process.exit(1);
115
+ flushStdioThenExit(1);
91
116
  }
92
117
  });
93
118
 
@@ -100,12 +125,13 @@ configCmd
100
125
  const keys = Object.keys(c);
101
126
  if (keys.length === 0) {
102
127
  console.log('No config set. Use: felo config set FELO_API_KEY <key>');
103
- return;
128
+ } else {
129
+ keys.forEach((k) => console.log(k));
104
130
  }
105
- keys.forEach((k) => console.log(k));
131
+ flushStdioThenExit(0);
106
132
  } catch (e) {
107
133
  console.error('Error:', e.message);
108
- process.exit(1);
134
+ flushStdioThenExit(1);
109
135
  }
110
136
  });
111
137
 
@@ -116,9 +142,10 @@ configCmd
116
142
  try {
117
143
  await config.unsetConfig(key);
118
144
  console.log(`Unset ${key}`);
145
+ flushStdioThenExit(0);
119
146
  } catch (e) {
120
147
  console.error('Error:', e.message);
121
- process.exit(1);
148
+ flushStdioThenExit(1);
122
149
  }
123
150
  });
124
151
 
@@ -127,6 +154,7 @@ configCmd
127
154
  .description('Show config file path')
128
155
  .action(() => {
129
156
  console.log(config.getConfigPath());
157
+ flushStdioThenExit(0);
130
158
  });
131
159
 
132
160
  program
@@ -135,7 +163,7 @@ program
135
163
  .argument('[input]', 'text or URL to summarize')
136
164
  .action(() => {
137
165
  console.error('summarize: not yet implemented. Use felo search for now.');
138
- process.exit(1);
166
+ flushStdioThenExit(1);
139
167
  });
140
168
 
141
169
  program
@@ -144,7 +172,7 @@ program
144
172
  .argument('[text]', 'text to translate')
145
173
  .action(() => {
146
174
  console.error('translate: not yet implemented. Use felo search for now.');
147
- process.exit(1);
175
+ flushStdioThenExit(1);
148
176
  });
149
177
 
150
178
  program.parse();
package/src/search.js CHANGED
@@ -38,7 +38,6 @@ async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOU
38
38
  ...options,
39
39
  signal: controller.signal,
40
40
  });
41
- clearTimeout(timeoutId);
42
41
  // Retry on 5xx (server errors)
43
42
  if (res.status >= 500 && attempt < MAX_RETRIES) {
44
43
  const delay = RETRY_BASE_MS * Math.pow(2, attempt);
@@ -47,7 +46,6 @@ async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOU
47
46
  }
48
47
  return res;
49
48
  } catch (err) {
50
- clearTimeout(timeoutId);
51
49
  lastError = err;
52
50
  if (err.name === 'AbortError') {
53
51
  throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
@@ -58,6 +56,8 @@ async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOU
58
56
  continue;
59
57
  }
60
58
  throw lastError;
59
+ } finally {
60
+ clearTimeout(timeoutId);
61
61
  }
62
62
  }
63
63
  throw lastError;
package/src/slides.js CHANGED
@@ -80,7 +80,7 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
80
80
  }
81
81
 
82
82
  /**
83
- * Get task historical info. Returns { task_status, live_doc_url?, live_doc_short_id? } or throws.
83
+ * Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
84
84
  * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
85
85
  */
86
86
  async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
@@ -116,6 +116,8 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
116
116
 
117
117
  return {
118
118
  task_status: payload.task_status ?? payload.status,
119
+ ppt_url: payload.ppt_url,
120
+ ppt_biz_id: payload.ppt_biz_id,
119
121
  live_doc_url: payload.live_doc_url,
120
122
  live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
121
123
  };
@@ -166,19 +168,25 @@ export async function slides(query, options = {}) {
166
168
  const normalizedStatus = normalizeTaskStatus(historical.task_status);
167
169
  lastStatus = normalizedStatus || historical.task_status;
168
170
 
171
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
169
172
  if (useLiveStatus) {
170
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
171
173
  spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
172
174
  writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed);
175
+ } else if (!options.verbose && !options.json) {
176
+ process.stderr.write(` Generating... ${elapsed}s\n`);
173
177
  }
174
178
 
175
179
  const done = normalizedStatus === 'COMPLETED' || normalizedStatus === 'SUCCESS';
176
180
  if (done) {
177
181
  if (useLiveStatus) clearStatusLine();
178
- const url =
182
+ const pptUrl =
183
+ historical.ppt_url ||
184
+ (historical.ppt_biz_id ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}` : null);
185
+ const liveDocUrl =
179
186
  historical.live_doc_url ||
180
187
  (historical.live_doc_short_id ? `https://felo.ai/livedoc/${historical.live_doc_short_id}` : null) ||
181
188
  (createResult.livedoc_short_id ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}` : null);
189
+ const url = pptUrl || liveDocUrl;
182
190
  if (options.json) {
183
191
  console.log(
184
192
  JSON.stringify(
@@ -187,8 +195,10 @@ export async function slides(query, options = {}) {
187
195
  data: {
188
196
  task_id: taskId,
189
197
  task_status: normalizedStatus || historical.task_status,
190
- live_doc_url: url,
191
- livedoc_short_id: createResult.livedoc_short_id,
198
+ ppt_url: pptUrl,
199
+ ppt_biz_id: historical.ppt_biz_id ?? createResult.ppt_business_id,
200
+ live_doc_url: liveDocUrl,
201
+ livedoc_short_id: historical.live_doc_short_id ?? createResult.livedoc_short_id,
192
202
  ppt_business_id: createResult.ppt_business_id,
193
203
  },
194
204
  },
@@ -198,10 +208,13 @@ export async function slides(query, options = {}) {
198
208
  );
199
209
  } else {
200
210
  if (url) {
201
- console.log(url);
211
+ if (pptUrl && !options.json) {
212
+ process.stderr.write('PPT ready. Open this link to preview:\n');
213
+ }
214
+ console.log(pptUrl || liveDocUrl);
202
215
  } else {
203
216
  if (useLiveStatus) clearStatusLine();
204
- console.error('Error: Completed but no live_doc_url in response');
217
+ console.error('Error: Completed but no ppt_url or live_doc_url in response');
205
218
  return 1;
206
219
  }
207
220
  }