felo-ai 0.2.4 → 0.2.5

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +1 -1
  3. package/src/slides.js +141 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Felo AI CLI - real-time search and PPT generation from the terminal",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from 'module';
4
4
  import { Command } from 'commander';
package/src/slides.js CHANGED
@@ -1,22 +1,26 @@
1
- import { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE } from './search.js';
1
+ import {
2
+ getApiKey,
3
+ fetchWithTimeoutAndRetry,
4
+ NO_KEY_MESSAGE,
5
+ } from "./search.js";
2
6
 
3
- const DEFAULT_API_BASE = 'https://openapi.felo.ai';
7
+ const DEFAULT_API_BASE = "https://openapi.felo.ai";
4
8
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
5
9
  const POLL_INTERVAL_MS = 10_000;
6
10
  const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
7
11
 
8
- const SPINNER_FRAMES = ['', '', '', '', '', '', '', '', '', ''];
12
+ const SPINNER_FRAMES = ["", "", "", "", "", "", "", "", "", ""];
9
13
  const STATUS_LINE_PAD = 50;
10
14
 
11
15
  /** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
12
16
  async function getApiBase() {
13
17
  let base = process.env.FELO_API_BASE?.trim();
14
18
  if (!base) {
15
- const { getConfigValue } = await import('./config.js');
16
- const v = await getConfigValue('FELO_API_BASE');
17
- base = typeof v === 'string' ? v.trim() : '';
19
+ const { getConfigValue } = await import("./config.js");
20
+ const v = await getConfigValue("FELO_API_BASE");
21
+ base = typeof v === "string" ? v.trim() : "";
18
22
  }
19
- const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, '');
23
+ const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
20
24
  return normalized;
21
25
  }
22
26
 
@@ -24,19 +28,21 @@ function sleep(ms) {
24
28
  return new Promise((resolve) => setTimeout(resolve, ms));
25
29
  }
26
30
 
27
- /** Write a single overwritable status line: spinner + elapsed. Call clearStatusLine() before next stdout. */
28
- function writeStatusLine(spinnerFrame, elapsedSec) {
31
+ /** Write status line: spinner + elapsed. When overwrite is true use \\r (TTY); else use \\n so non-TTY still shows animation. */
32
+ function writeStatusLine(spinnerFrame, elapsedSec, overwrite = true) {
29
33
  const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
30
- const padded = s.padEnd(STATUS_LINE_PAD, ' ');
31
- process.stderr.write(`\r${padded}`);
34
+ const padded = s.padEnd(STATUS_LINE_PAD, " ");
35
+ process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
32
36
  }
33
37
 
34
38
  function clearStatusLine() {
35
- process.stderr.write(`\r${' '.repeat(STATUS_LINE_PAD)}\r`);
39
+ process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
36
40
  }
37
41
 
38
42
  function normalizeTaskStatus(status) {
39
- return String(status || '').trim().toUpperCase();
43
+ return String(status || "")
44
+ .trim()
45
+ .toUpperCase();
40
46
  }
41
47
 
42
48
  /**
@@ -48,11 +54,11 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
48
54
  const res = await fetchWithTimeoutAndRetry(
49
55
  url,
50
56
  {
51
- method: 'POST',
57
+ method: "POST",
52
58
  headers: {
53
- 'Accept': 'application/json',
54
- 'Authorization': `Bearer ${apiKey}`,
55
- 'Content-Type': 'application/json',
59
+ Accept: "application/json",
60
+ Authorization: `Bearer ${apiKey}`,
61
+ "Content-Type": "application/json",
56
62
  },
57
63
  body: JSON.stringify({ query: query.trim() }),
58
64
  },
@@ -61,19 +67,20 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
61
67
 
62
68
  const data = await res.json().catch(() => ({}));
63
69
 
64
- if (data.status === 'error') {
65
- const msg = data.message || data.code || 'Unknown error';
70
+ if (data.status === "error") {
71
+ const msg = data.message || data.code || "Unknown error";
66
72
  throw new Error(msg);
67
73
  }
68
74
 
69
75
  if (!res.ok) {
70
- const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
76
+ const msg =
77
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
71
78
  throw new Error(msg);
72
79
  }
73
80
 
74
81
  const payload = data.data;
75
82
  if (!payload || !payload.task_id) {
76
- throw new Error('Unexpected response: missing task_id');
83
+ throw new Error("Unexpected response: missing task_id");
77
84
  }
78
85
 
79
86
  return payload;
@@ -88,10 +95,10 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
88
95
  const res = await fetchWithTimeoutAndRetry(
89
96
  url,
90
97
  {
91
- method: 'GET',
98
+ method: "GET",
92
99
  headers: {
93
- 'Accept': 'application/json',
94
- 'Authorization': `Bearer ${apiKey}`,
100
+ Accept: "application/json",
101
+ Authorization: `Bearer ${apiKey}`,
95
102
  },
96
103
  },
97
104
  timeoutMs
@@ -99,19 +106,20 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
99
106
 
100
107
  const data = await res.json().catch(() => ({}));
101
108
 
102
- if (data.status === 'error') {
103
- const msg = data.message || data.code || 'Unknown error';
109
+ if (data.status === "error") {
110
+ const msg = data.message || data.code || "Unknown error";
104
111
  throw new Error(msg);
105
112
  }
106
113
 
107
114
  if (!res.ok) {
108
- const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
115
+ const msg =
116
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
109
117
  throw new Error(msg);
110
118
  }
111
119
 
112
120
  const payload = data.data;
113
121
  if (!payload) {
114
- throw new Error('Unexpected response: missing data');
122
+ throw new Error("Unexpected response: missing data");
115
123
  }
116
124
 
117
125
  return {
@@ -120,6 +128,7 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
120
128
  ppt_biz_id: payload.ppt_biz_id,
121
129
  live_doc_url: payload.live_doc_url,
122
130
  live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
131
+ error_message: payload.error_message,
123
132
  };
124
133
  }
125
134
 
@@ -141,64 +150,85 @@ export async function slides(query, options = {}) {
141
150
  try {
142
151
  const apiBase = await getApiBase();
143
152
 
144
- process.stderr.write('Creating PPT task...\n');
153
+ process.stderr.write("Creating PPT task...\n");
145
154
 
146
- const createResult = await createPptTask(apiKey, query, requestTimeoutMs, apiBase);
155
+ const createResult = await createPptTask(
156
+ apiKey,
157
+ query,
158
+ requestTimeoutMs,
159
+ apiBase
160
+ );
147
161
  const taskId = createResult.task_id;
148
162
 
149
163
  if (options.json && options.verbose) {
150
164
  process.stderr.write(`Task ID: ${taskId}\n`);
151
165
  }
152
166
 
153
- const useLiveStatus =
154
- process.stderr.isTTY && !options.verbose && !options.json;
167
+ // 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
168
+ const useLiveStatus = !options.verbose && !options.json;
169
+ const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
155
170
  if (!useLiveStatus) {
156
- process.stderr.write('Generating slides (this may take a minute)...\n');
171
+ process.stderr.write("Generating slides (this may take a minute)...\n");
157
172
  }
158
173
 
159
174
  const startTime = Date.now();
160
175
  let lastStatus;
161
176
  let spinIndex = 0;
162
- if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0);
177
+ if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
163
178
 
164
179
  while (Date.now() - startTime < pollTimeoutMs) {
165
180
  await sleep(pollIntervalMs);
166
181
 
167
- const historical = await getTaskHistorical(apiKey, taskId, requestTimeoutMs, apiBase);
182
+ const historical = await getTaskHistorical(
183
+ apiKey,
184
+ taskId,
185
+ requestTimeoutMs,
186
+ apiBase
187
+ );
168
188
  const normalizedStatus = normalizeTaskStatus(historical.task_status);
169
189
  lastStatus = normalizedStatus || historical.task_status;
170
190
 
171
191
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
172
192
  if (useLiveStatus) {
173
193
  spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
174
- writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed);
194
+ writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
175
195
  } else if (!options.verbose && !options.json) {
176
196
  process.stderr.write(` Generating... ${elapsed}s\n`);
177
197
  }
178
198
 
179
- const done = normalizedStatus === 'COMPLETED' || normalizedStatus === 'SUCCESS';
199
+ const done =
200
+ normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
180
201
  if (done) {
181
- if (useLiveStatus) clearStatusLine();
202
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
182
203
  const pptUrl =
183
204
  historical.ppt_url ||
184
- (historical.ppt_biz_id ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}` : null);
205
+ (historical.ppt_biz_id
206
+ ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
207
+ : null);
185
208
  const liveDocUrl =
186
209
  historical.live_doc_url ||
187
- (historical.live_doc_short_id ? `https://felo.ai/livedoc/${historical.live_doc_short_id}` : null) ||
188
- (createResult.livedoc_short_id ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}` : null);
210
+ (historical.live_doc_short_id
211
+ ? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
212
+ : null) ||
213
+ (createResult.livedoc_short_id
214
+ ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
215
+ : null);
189
216
  const url = pptUrl || liveDocUrl;
190
217
  if (options.json) {
191
218
  console.log(
192
219
  JSON.stringify(
193
220
  {
194
- status: 'ok',
221
+ status: "ok",
195
222
  data: {
196
223
  task_id: taskId,
197
224
  task_status: normalizedStatus || historical.task_status,
198
225
  ppt_url: pptUrl,
199
- ppt_biz_id: historical.ppt_biz_id ?? createResult.ppt_business_id,
226
+ ppt_biz_id:
227
+ historical.ppt_biz_id ?? createResult.ppt_business_id,
200
228
  live_doc_url: liveDocUrl,
201
- livedoc_short_id: historical.live_doc_short_id ?? createResult.livedoc_short_id,
229
+ livedoc_short_id:
230
+ historical.live_doc_short_id ??
231
+ createResult.livedoc_short_id,
202
232
  ppt_business_id: createResult.ppt_business_id,
203
233
  },
204
234
  },
@@ -209,40 +239,94 @@ export async function slides(query, options = {}) {
209
239
  } else {
210
240
  if (url) {
211
241
  if (pptUrl && !options.json) {
212
- process.stderr.write('PPT ready. Open this link to preview:\n');
242
+ process.stderr.write("PPT ready. Open this link to preview:\n");
213
243
  }
214
244
  console.log(pptUrl || liveDocUrl);
215
245
  } else {
216
246
  if (useLiveStatus) clearStatusLine();
217
- console.error('Error: Completed but no ppt_url or live_doc_url in response');
247
+ console.error(
248
+ "Error: Completed but no ppt_url or live_doc_url in response"
249
+ );
218
250
  return 1;
219
251
  }
220
252
  }
221
253
  return 0;
222
254
  }
223
255
 
224
- if (
225
- normalizedStatus === 'FAILED' ||
226
- normalizedStatus === 'ERROR'
227
- ) {
228
- if (useLiveStatus) clearStatusLine();
229
- console.error(`Error: Task finished with status: ${normalizedStatus || historical.task_status}`);
256
+ const terminalErrorStatuses = [
257
+ "FAILED",
258
+ "PENDING",
259
+ "EXPIRED",
260
+ "CANCELED",
261
+ ];
262
+
263
+ if (terminalErrorStatuses.includes(normalizedStatus)) {
264
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
265
+
266
+ const liveDocUrl =
267
+ historical.live_doc_url ||
268
+ (historical.live_doc_short_id
269
+ ? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
270
+ : null) ||
271
+ (createResult.livedoc_short_id
272
+ ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
273
+ : null);
274
+
275
+ const errorMessage =
276
+ historical.error_message ||
277
+ `Task finished with status: ${
278
+ normalizedStatus || historical.task_status
279
+ }`;
280
+
281
+ if (options.json) {
282
+ console.log(
283
+ JSON.stringify(
284
+ {
285
+ status: "error",
286
+ data: {
287
+ task_id: taskId,
288
+ task_status: normalizedStatus || historical.task_status,
289
+ live_doc_url: liveDocUrl,
290
+ livedoc_short_id:
291
+ historical.live_doc_short_id ??
292
+ createResult.livedoc_short_id,
293
+ error_message: errorMessage,
294
+ },
295
+ },
296
+ null,
297
+ 2
298
+ )
299
+ );
300
+ } else {
301
+ if (liveDocUrl) {
302
+ console.log(liveDocUrl);
303
+ }
304
+ console.error(errorMessage);
305
+ }
306
+
230
307
  return 1;
231
308
  }
232
309
 
233
310
  if (options.verbose) {
234
- process.stderr.write(` Status: ${normalizedStatus || historical.task_status || 'UNKNOWN'}\n`);
311
+ process.stderr.write(
312
+ ` Status: ${
313
+ normalizedStatus || historical.task_status || "UNKNOWN"
314
+ }\n`
315
+ );
235
316
  }
236
317
  }
237
318
 
238
- if (useLiveStatus) clearStatusLine();
319
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
239
320
  console.error(
240
- `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${lastStatus ?? 'unknown'}`
321
+ `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
322
+ lastStatus ?? "unknown"
323
+ }`
241
324
  );
242
325
  return 1;
243
326
  } catch (err) {
244
- if (process.stderr.isTTY && !options.verbose && !options.json) clearStatusLine();
245
- console.error('Error:', err.message || err);
327
+ if (process.stderr.isTTY && !options.verbose && !options.json)
328
+ clearStatusLine();
329
+ console.error("Error:", err.message || err);
246
330
  return 1;
247
331
  }
248
332
  }