felo-ai 0.2.6 → 0.2.9

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 (39) hide show
  1. package/.github/workflows/publish-npm.yml +39 -0
  2. package/CHANGELOG.md +30 -0
  3. package/CONTRIBUTING.md +346 -346
  4. package/README.en.md +129 -129
  5. package/README.md +435 -408
  6. package/docs/EXAMPLES.md +632 -632
  7. package/docs/FAQ.md +479 -479
  8. package/felo-search/LICENSE +21 -21
  9. package/felo-search/README.md +440 -440
  10. package/felo-search/SKILL.md +291 -291
  11. package/felo-slides/LICENSE +21 -21
  12. package/felo-slides/README.md +87 -87
  13. package/felo-slides/SKILL.md +166 -166
  14. package/felo-slides/scripts/run_ppt_task.mjs +251 -251
  15. package/felo-superAgent/LICENSE +21 -0
  16. package/felo-superAgent/README.md +125 -0
  17. package/felo-superAgent/SKILL.md +165 -0
  18. package/felo-web-fetch/README.md +127 -0
  19. package/felo-web-fetch/SKILL.md +204 -0
  20. package/felo-web-fetch/scripts/run_web_fetch.mjs +316 -0
  21. package/felo-x-search/SKILL.md +204 -0
  22. package/felo-x-search/scripts/run_x_search.mjs +385 -0
  23. package/felo-youtube-subtitling/README.md +59 -59
  24. package/felo-youtube-subtitling/SKILL.md +161 -161
  25. package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +239 -239
  26. package/package.json +37 -35
  27. package/src/cli.js +370 -252
  28. package/src/config.js +66 -66
  29. package/src/search.js +142 -142
  30. package/src/slides.js +332 -332
  31. package/src/superAgent.js +609 -0
  32. package/src/{webExtract.js → webFetch.js} +148 -148
  33. package/src/xSearch.js +366 -0
  34. package/src/youtubeSubtitling.js +179 -179
  35. package/tests/config.test.js +78 -78
  36. package/tests/search.test.js +100 -100
  37. package/felo-web-extract/README.md +0 -78
  38. package/felo-web-extract/SKILL.md +0 -200
  39. package/felo-web-extract/scripts/run_web_extract.mjs +0 -232
package/src/slides.js CHANGED
@@ -1,332 +1,332 @@
1
- import {
2
- getApiKey,
3
- fetchWithTimeoutAndRetry,
4
- NO_KEY_MESSAGE,
5
- } from "./search.js";
6
-
7
- const DEFAULT_API_BASE = "https://openapi.felo.ai";
8
- const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
9
- const POLL_INTERVAL_MS = 10_000;
10
- const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
11
-
12
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
- const STATUS_LINE_PAD = 50;
14
-
15
- /** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
16
- async function getApiBase() {
17
- let base = process.env.FELO_API_BASE?.trim();
18
- if (!base) {
19
- const { getConfigValue } = await import("./config.js");
20
- const v = await getConfigValue("FELO_API_BASE");
21
- base = typeof v === "string" ? v.trim() : "";
22
- }
23
- const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
24
- return normalized;
25
- }
26
-
27
- function sleep(ms) {
28
- return new Promise((resolve) => setTimeout(resolve, ms));
29
- }
30
-
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) {
33
- const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
34
- const padded = s.padEnd(STATUS_LINE_PAD, " ");
35
- process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
36
- }
37
-
38
- function clearStatusLine() {
39
- process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
40
- }
41
-
42
- function normalizeTaskStatus(status) {
43
- return String(status || "")
44
- .trim()
45
- .toUpperCase();
46
- }
47
-
48
- /**
49
- * Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
50
- * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
51
- */
52
- async function createPptTask(apiKey, query, timeoutMs, apiBase) {
53
- const url = `${apiBase}/v2/ppts`;
54
- const res = await fetchWithTimeoutAndRetry(
55
- url,
56
- {
57
- method: "POST",
58
- headers: {
59
- Accept: "application/json",
60
- Authorization: `Bearer ${apiKey}`,
61
- "Content-Type": "application/json",
62
- },
63
- body: JSON.stringify({ query: query.trim() }),
64
- },
65
- timeoutMs
66
- );
67
-
68
- const data = await res.json().catch(() => ({}));
69
-
70
- if (data.status === "error") {
71
- const msg = data.message || data.code || "Unknown error";
72
- throw new Error(msg);
73
- }
74
-
75
- if (!res.ok) {
76
- const msg =
77
- data.message || data.error || res.statusText || `HTTP ${res.status}`;
78
- throw new Error(msg);
79
- }
80
-
81
- const payload = data.data;
82
- if (!payload || !payload.task_id) {
83
- throw new Error("Unexpected response: missing task_id");
84
- }
85
-
86
- return payload;
87
- }
88
-
89
- /**
90
- * Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
91
- * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
92
- */
93
- async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
94
- const url = `${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`;
95
- const res = await fetchWithTimeoutAndRetry(
96
- url,
97
- {
98
- method: "GET",
99
- headers: {
100
- Accept: "application/json",
101
- Authorization: `Bearer ${apiKey}`,
102
- },
103
- },
104
- timeoutMs
105
- );
106
-
107
- const data = await res.json().catch(() => ({}));
108
-
109
- if (data.status === "error") {
110
- const msg = data.message || data.code || "Unknown error";
111
- throw new Error(msg);
112
- }
113
-
114
- if (!res.ok) {
115
- const msg =
116
- data.message || data.error || res.statusText || `HTTP ${res.status}`;
117
- throw new Error(msg);
118
- }
119
-
120
- const payload = data.data;
121
- if (!payload) {
122
- throw new Error("Unexpected response: missing data");
123
- }
124
-
125
- return {
126
- task_status: payload.task_status ?? payload.status,
127
- ppt_url: payload.ppt_url,
128
- ppt_biz_id: payload.ppt_biz_id,
129
- live_doc_url: payload.live_doc_url,
130
- live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
131
- error_message: payload.error_message,
132
- };
133
- }
134
-
135
- /**
136
- * Run slides: create PPT task, poll until terminal state, output URL or error.
137
- * @returns {Promise<number>} exit code (0 success, 1 failure)
138
- */
139
- export async function slides(query, options = {}) {
140
- const apiKey = await getApiKey();
141
- if (!apiKey) {
142
- console.error(NO_KEY_MESSAGE.trim());
143
- return 1;
144
- }
145
-
146
- const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
147
- const pollTimeoutMs = options.pollTimeoutMs ?? MAX_POLL_TIMEOUT_MS;
148
- const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
149
-
150
- try {
151
- const apiBase = await getApiBase();
152
-
153
- process.stderr.write("Creating PPT task...\n");
154
-
155
- const createResult = await createPptTask(
156
- apiKey,
157
- query,
158
- requestTimeoutMs,
159
- apiBase
160
- );
161
- const taskId = createResult.task_id;
162
-
163
- if (options.json && options.verbose) {
164
- process.stderr.write(`Task ID: ${taskId}\n`);
165
- }
166
-
167
- // 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
168
- const useLiveStatus = !options.verbose && !options.json;
169
- const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
170
- if (!useLiveStatus) {
171
- process.stderr.write("Generating slides (this may take a minute)...\n");
172
- }
173
-
174
- const startTime = Date.now();
175
- let lastStatus;
176
- let spinIndex = 0;
177
- if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
178
-
179
- while (Date.now() - startTime < pollTimeoutMs) {
180
- await sleep(pollIntervalMs);
181
-
182
- const historical = await getTaskHistorical(
183
- apiKey,
184
- taskId,
185
- requestTimeoutMs,
186
- apiBase
187
- );
188
- const normalizedStatus = normalizeTaskStatus(historical.task_status);
189
- lastStatus = normalizedStatus || historical.task_status;
190
-
191
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
192
- if (useLiveStatus) {
193
- spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
194
- writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
195
- } else if (!options.verbose && !options.json) {
196
- process.stderr.write(` Generating... ${elapsed}s\n`);
197
- }
198
-
199
- const done =
200
- normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
201
- if (done) {
202
- if (useLiveStatus && canOverwriteLine) clearStatusLine();
203
- const pptUrl =
204
- historical.ppt_url ||
205
- (historical.ppt_biz_id
206
- ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
207
- : null);
208
- const liveDocUrl =
209
- historical.live_doc_url ||
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);
216
- const url = pptUrl || liveDocUrl;
217
- if (options.json) {
218
- console.log(
219
- JSON.stringify(
220
- {
221
- status: "ok",
222
- data: {
223
- task_id: taskId,
224
- task_status: normalizedStatus || historical.task_status,
225
- ppt_url: pptUrl,
226
- ppt_biz_id:
227
- historical.ppt_biz_id ?? createResult.ppt_business_id,
228
- live_doc_url: liveDocUrl,
229
- livedoc_short_id:
230
- historical.live_doc_short_id ??
231
- createResult.livedoc_short_id,
232
- ppt_business_id: createResult.ppt_business_id,
233
- },
234
- },
235
- null,
236
- 2
237
- )
238
- );
239
- } else {
240
- if (url) {
241
- if (pptUrl && !options.json) {
242
- process.stderr.write("PPT ready. Open this link to preview:\n");
243
- }
244
- console.log(pptUrl || liveDocUrl);
245
- } else {
246
- if (useLiveStatus) clearStatusLine();
247
- console.error(
248
- "Error: Completed but no ppt_url or live_doc_url in response"
249
- );
250
- return 1;
251
- }
252
- }
253
- return 0;
254
- }
255
-
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
-
307
- return 1;
308
- }
309
-
310
- if (options.verbose) {
311
- process.stderr.write(
312
- ` Status: ${
313
- normalizedStatus || historical.task_status || "UNKNOWN"
314
- }\n`
315
- );
316
- }
317
- }
318
-
319
- if (useLiveStatus && canOverwriteLine) clearStatusLine();
320
- console.error(
321
- `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
322
- lastStatus ?? "unknown"
323
- }`
324
- );
325
- return 1;
326
- } catch (err) {
327
- if (process.stderr.isTTY && !options.verbose && !options.json)
328
- clearStatusLine();
329
- console.error("Error:", err.message || err);
330
- return 1;
331
- }
332
- }
1
+ import {
2
+ getApiKey,
3
+ fetchWithTimeoutAndRetry,
4
+ NO_KEY_MESSAGE,
5
+ } from "./search.js";
6
+
7
+ const DEFAULT_API_BASE = "https://openapi.felo.ai";
8
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
9
+ const POLL_INTERVAL_MS = 10_000;
10
+ const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
11
+
12
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
+ const STATUS_LINE_PAD = 50;
14
+
15
+ /** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
16
+ async function getApiBase() {
17
+ let base = process.env.FELO_API_BASE?.trim();
18
+ if (!base) {
19
+ const { getConfigValue } = await import("./config.js");
20
+ const v = await getConfigValue("FELO_API_BASE");
21
+ base = typeof v === "string" ? v.trim() : "";
22
+ }
23
+ const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
24
+ return normalized;
25
+ }
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
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) {
33
+ const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
34
+ const padded = s.padEnd(STATUS_LINE_PAD, " ");
35
+ process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
36
+ }
37
+
38
+ function clearStatusLine() {
39
+ process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
40
+ }
41
+
42
+ function normalizeTaskStatus(status) {
43
+ return String(status || "")
44
+ .trim()
45
+ .toUpperCase();
46
+ }
47
+
48
+ /**
49
+ * Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
50
+ * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
51
+ */
52
+ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
53
+ const url = `${apiBase}/v2/ppts`;
54
+ const res = await fetchWithTimeoutAndRetry(
55
+ url,
56
+ {
57
+ method: "POST",
58
+ headers: {
59
+ Accept: "application/json",
60
+ Authorization: `Bearer ${apiKey}`,
61
+ "Content-Type": "application/json",
62
+ },
63
+ body: JSON.stringify({ query: query.trim() }),
64
+ },
65
+ timeoutMs
66
+ );
67
+
68
+ const data = await res.json().catch(() => ({}));
69
+
70
+ if (data.status === "error") {
71
+ const msg = data.message || data.code || "Unknown error";
72
+ throw new Error(msg);
73
+ }
74
+
75
+ if (!res.ok) {
76
+ const msg =
77
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
78
+ throw new Error(msg);
79
+ }
80
+
81
+ const payload = data.data;
82
+ if (!payload || !payload.task_id) {
83
+ throw new Error("Unexpected response: missing task_id");
84
+ }
85
+
86
+ return payload;
87
+ }
88
+
89
+ /**
90
+ * Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
91
+ * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
92
+ */
93
+ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
94
+ const url = `${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`;
95
+ const res = await fetchWithTimeoutAndRetry(
96
+ url,
97
+ {
98
+ method: "GET",
99
+ headers: {
100
+ Accept: "application/json",
101
+ Authorization: `Bearer ${apiKey}`,
102
+ },
103
+ },
104
+ timeoutMs
105
+ );
106
+
107
+ const data = await res.json().catch(() => ({}));
108
+
109
+ if (data.status === "error") {
110
+ const msg = data.message || data.code || "Unknown error";
111
+ throw new Error(msg);
112
+ }
113
+
114
+ if (!res.ok) {
115
+ const msg =
116
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
117
+ throw new Error(msg);
118
+ }
119
+
120
+ const payload = data.data;
121
+ if (!payload) {
122
+ throw new Error("Unexpected response: missing data");
123
+ }
124
+
125
+ return {
126
+ task_status: payload.task_status ?? payload.status,
127
+ ppt_url: payload.ppt_url,
128
+ ppt_biz_id: payload.ppt_biz_id,
129
+ live_doc_url: payload.live_doc_url,
130
+ live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
131
+ error_message: payload.error_message,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Run slides: create PPT task, poll until terminal state, output URL or error.
137
+ * @returns {Promise<number>} exit code (0 success, 1 failure)
138
+ */
139
+ export async function slides(query, options = {}) {
140
+ const apiKey = await getApiKey();
141
+ if (!apiKey) {
142
+ console.error(NO_KEY_MESSAGE.trim());
143
+ return 1;
144
+ }
145
+
146
+ const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
147
+ const pollTimeoutMs = options.pollTimeoutMs ?? MAX_POLL_TIMEOUT_MS;
148
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
149
+
150
+ try {
151
+ const apiBase = await getApiBase();
152
+
153
+ process.stderr.write("Creating PPT task...\n");
154
+
155
+ const createResult = await createPptTask(
156
+ apiKey,
157
+ query,
158
+ requestTimeoutMs,
159
+ apiBase
160
+ );
161
+ const taskId = createResult.task_id;
162
+
163
+ if (options.json && options.verbose) {
164
+ process.stderr.write(`Task ID: ${taskId}\n`);
165
+ }
166
+
167
+ // 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
168
+ const useLiveStatus = !options.verbose && !options.json;
169
+ const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
170
+ if (!useLiveStatus) {
171
+ process.stderr.write("Generating slides (this may take a minute)...\n");
172
+ }
173
+
174
+ const startTime = Date.now();
175
+ let lastStatus;
176
+ let spinIndex = 0;
177
+ if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
178
+
179
+ while (Date.now() - startTime < pollTimeoutMs) {
180
+ await sleep(pollIntervalMs);
181
+
182
+ const historical = await getTaskHistorical(
183
+ apiKey,
184
+ taskId,
185
+ requestTimeoutMs,
186
+ apiBase
187
+ );
188
+ const normalizedStatus = normalizeTaskStatus(historical.task_status);
189
+ lastStatus = normalizedStatus || historical.task_status;
190
+
191
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
192
+ if (useLiveStatus) {
193
+ spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
194
+ writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
195
+ } else if (!options.verbose && !options.json) {
196
+ process.stderr.write(` Generating... ${elapsed}s\n`);
197
+ }
198
+
199
+ const done =
200
+ normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
201
+ if (done) {
202
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
203
+ const pptUrl =
204
+ historical.ppt_url ||
205
+ (historical.ppt_biz_id
206
+ ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
207
+ : null);
208
+ const liveDocUrl =
209
+ historical.live_doc_url ||
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);
216
+ const url = pptUrl || liveDocUrl;
217
+ if (options.json) {
218
+ console.log(
219
+ JSON.stringify(
220
+ {
221
+ status: "ok",
222
+ data: {
223
+ task_id: taskId,
224
+ task_status: normalizedStatus || historical.task_status,
225
+ ppt_url: pptUrl,
226
+ ppt_biz_id:
227
+ historical.ppt_biz_id ?? createResult.ppt_business_id,
228
+ live_doc_url: liveDocUrl,
229
+ livedoc_short_id:
230
+ historical.live_doc_short_id ??
231
+ createResult.livedoc_short_id,
232
+ ppt_business_id: createResult.ppt_business_id,
233
+ },
234
+ },
235
+ null,
236
+ 2
237
+ )
238
+ );
239
+ } else {
240
+ if (url) {
241
+ if (pptUrl && !options.json) {
242
+ process.stderr.write("PPT ready. Open this link to preview:\n");
243
+ }
244
+ console.log(pptUrl || liveDocUrl);
245
+ } else {
246
+ if (useLiveStatus) clearStatusLine();
247
+ console.error(
248
+ "Error: Completed but no ppt_url or live_doc_url in response"
249
+ );
250
+ return 1;
251
+ }
252
+ }
253
+ return 0;
254
+ }
255
+
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
+
307
+ return 1;
308
+ }
309
+
310
+ if (options.verbose) {
311
+ process.stderr.write(
312
+ ` Status: ${
313
+ normalizedStatus || historical.task_status || "UNKNOWN"
314
+ }\n`
315
+ );
316
+ }
317
+ }
318
+
319
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
320
+ console.error(
321
+ `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
322
+ lastStatus ?? "unknown"
323
+ }`
324
+ );
325
+ return 1;
326
+ } catch (err) {
327
+ if (process.stderr.isTTY && !options.verbose && !options.json)
328
+ clearStatusLine();
329
+ console.error("Error:", err.message || err);
330
+ return 1;
331
+ }
332
+ }