felo-ai 0.2.0 → 0.2.2
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.
- package/README.en.md +2 -1
- package/README.md +48 -48
- package/docs/EXAMPLES.md +143 -192
- package/docs/FAQ.md +4 -4
- package/felo-search/README.md +92 -147
- package/felo-search/SKILL.md +17 -18
- package/felo-slides/LICENSE +21 -0
- package/felo-slides/README.md +87 -0
- package/felo-slides/SKILL.md +154 -0
- package/felo-slides/scripts/run_ppt_task.mjs +251 -0
- package/package.json +2 -2
- package/src/cli.js +7 -5
- package/src/slides.js +39 -19
- package/README.ja.md +0 -87
- package/README.ko.md +0 -87
- package/README.zh-CN.md +0 -87
- package/README.zh-TW.md +0 -87
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
+
const DEFAULT_INTERVAL_SEC = 10;
|
|
5
|
+
const DEFAULT_MAX_WAIT_SEC = 1800;
|
|
6
|
+
const DEFAULT_TIMEOUT_SEC = 60;
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error(
|
|
10
|
+
[
|
|
11
|
+
'Usage:',
|
|
12
|
+
' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
|
|
13
|
+
'',
|
|
14
|
+
'Options:',
|
|
15
|
+
' --query <text> PPT prompt (required)',
|
|
16
|
+
' --interval <seconds> Poll interval, default 10',
|
|
17
|
+
' --max-wait <seconds> Max wait time, default 1800',
|
|
18
|
+
' --timeout <seconds> Request timeout, default 60',
|
|
19
|
+
' --json Print JSON output',
|
|
20
|
+
' --verbose Print polling status to stderr',
|
|
21
|
+
' --help Show this help',
|
|
22
|
+
].join('\n')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const out = {
|
|
28
|
+
query: '',
|
|
29
|
+
intervalSec: DEFAULT_INTERVAL_SEC,
|
|
30
|
+
maxWaitSec: DEFAULT_MAX_WAIT_SEC,
|
|
31
|
+
timeoutSec: DEFAULT_TIMEOUT_SEC,
|
|
32
|
+
json: false,
|
|
33
|
+
verbose: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === '--help' || a === '-h') {
|
|
39
|
+
out.help = true;
|
|
40
|
+
} else if (a === '--json') {
|
|
41
|
+
out.json = true;
|
|
42
|
+
} else if (a === '--verbose' || a === '-v') {
|
|
43
|
+
out.verbose = true;
|
|
44
|
+
} else if (a === '--query') {
|
|
45
|
+
out.query = argv[i + 1] ?? '';
|
|
46
|
+
i += 1;
|
|
47
|
+
} else if (a === '--interval') {
|
|
48
|
+
out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
49
|
+
i += 1;
|
|
50
|
+
} else if (a === '--max-wait') {
|
|
51
|
+
out.maxWaitSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (a === '--timeout') {
|
|
54
|
+
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (!a.startsWith('-') && !out.query) {
|
|
57
|
+
out.query = a;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!Number.isFinite(out.intervalSec) || out.intervalSec <= 0) out.intervalSec = DEFAULT_INTERVAL_SEC;
|
|
62
|
+
if (!Number.isFinite(out.maxWaitSec) || out.maxWaitSec <= 0) out.maxWaitSec = DEFAULT_MAX_WAIT_SEC;
|
|
63
|
+
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) out.timeoutSec = DEFAULT_TIMEOUT_SEC;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeStatus(v) {
|
|
68
|
+
return String(v ?? '').trim().toUpperCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sleep(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getMessage(payload) {
|
|
76
|
+
return (
|
|
77
|
+
payload?.message ||
|
|
78
|
+
payload?.error ||
|
|
79
|
+
payload?.msg ||
|
|
80
|
+
payload?.code ||
|
|
81
|
+
'Unknown error'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isApiError(payload) {
|
|
86
|
+
const status = payload?.status;
|
|
87
|
+
const code = payload?.code;
|
|
88
|
+
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
|
|
89
|
+
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
98
|
+
let body = {};
|
|
99
|
+
try {
|
|
100
|
+
body = await res.json();
|
|
101
|
+
} catch {
|
|
102
|
+
body = {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
|
|
107
|
+
}
|
|
108
|
+
if (isApiError(body)) {
|
|
109
|
+
throw new Error(getMessage(body));
|
|
110
|
+
}
|
|
111
|
+
return body;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
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
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function createTask(apiKey, apiBase, query, timeoutMs) {
|
|
132
|
+
const payload = await fetchJson(
|
|
133
|
+
`${apiBase}/v2/ppts`,
|
|
134
|
+
{
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
Accept: 'application/json',
|
|
138
|
+
Authorization: `Bearer ${apiKey}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({ query }),
|
|
142
|
+
},
|
|
143
|
+
timeoutMs
|
|
144
|
+
);
|
|
145
|
+
const data = payload?.data ?? {};
|
|
146
|
+
if (!data.task_id) {
|
|
147
|
+
throw new Error('Unexpected response: missing task_id');
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function queryHistorical(apiKey, apiBase, taskId, timeoutMs) {
|
|
153
|
+
const payload = await fetchJson(
|
|
154
|
+
`${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`,
|
|
155
|
+
{
|
|
156
|
+
method: 'GET',
|
|
157
|
+
headers: {
|
|
158
|
+
Accept: 'application/json',
|
|
159
|
+
Authorization: `Bearer ${apiKey}`,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
timeoutMs
|
|
163
|
+
);
|
|
164
|
+
return payload?.data ?? {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
if (args.help) {
|
|
170
|
+
usage();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
if (!args.query) {
|
|
174
|
+
usage();
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
179
|
+
if (!apiKey) {
|
|
180
|
+
console.error('ERROR: FELO_API_KEY not set');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
185
|
+
const timeoutMs = args.timeoutSec * 1000;
|
|
186
|
+
const intervalMs = args.intervalSec * 1000;
|
|
187
|
+
const maxWaitMs = args.maxWaitSec * 1000;
|
|
188
|
+
|
|
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}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const startAt = Date.now();
|
|
196
|
+
let lastStatus = '';
|
|
197
|
+
|
|
198
|
+
while (Date.now() - startAt <= maxWaitMs) {
|
|
199
|
+
const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
|
|
200
|
+
const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
|
|
201
|
+
const urls = extractTaskUrls(historicalData, createData);
|
|
202
|
+
lastStatus = taskStatus || 'UNKNOWN';
|
|
203
|
+
|
|
204
|
+
if (args.verbose) {
|
|
205
|
+
const elapsedSec = Math.floor((Date.now() - startAt) / 1000);
|
|
206
|
+
console.error(`[${elapsedSec}s] Status: ${lastStatus}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (taskStatus === 'COMPLETED' || taskStatus === 'SUCCESS') {
|
|
210
|
+
if (!urls.displayUrl) {
|
|
211
|
+
throw new Error('Task completed but no ppt_url/live_doc_url is available');
|
|
212
|
+
}
|
|
213
|
+
if (args.json) {
|
|
214
|
+
console.log(
|
|
215
|
+
JSON.stringify(
|
|
216
|
+
{
|
|
217
|
+
status: 'ok',
|
|
218
|
+
data: {
|
|
219
|
+
task_id: taskId,
|
|
220
|
+
task_status: taskStatus,
|
|
221
|
+
ppt_url: urls.pptUrl || null,
|
|
222
|
+
live_doc_url: urls.liveDocUrl || null,
|
|
223
|
+
livedoc_short_id: createData.livedoc_short_id ?? historicalData.live_doc_short_id ?? historicalData.livedoc_short_id ?? null,
|
|
224
|
+
ppt_business_id: createData.ppt_business_id ?? null,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(urls.displayUrl);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (taskStatus === 'FAILED' || taskStatus === 'ERROR') {
|
|
238
|
+
throw new Error(`Task finished with status: ${taskStatus}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await sleep(intervalMs);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`Timed out after ${args.maxWaitSec}s. Last status: ${lastStatus || 'UNKNOWN'}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main().catch((err) => {
|
|
248
|
+
console.error(`ERROR: ${err?.message || err}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
251
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Felo AI CLI - real-time search from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"felo-search": "^0.1.1"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "node --test tests
|
|
31
|
+
"test": "node --test \"tests/*.js\""
|
|
32
32
|
}
|
|
33
33
|
}
|
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';
|
|
@@ -30,7 +30,9 @@ program
|
|
|
30
30
|
verbose: opts.verbose,
|
|
31
31
|
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
32
32
|
});
|
|
33
|
-
process.
|
|
33
|
+
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);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
program
|
|
@@ -40,15 +42,15 @@ program
|
|
|
40
42
|
.option('-j, --json', 'output raw JSON with task_id and live_doc_url')
|
|
41
43
|
.option('-v, --verbose', 'show polling status')
|
|
42
44
|
.option('-t, --timeout <seconds>', 'request timeout in seconds for each API call', '60')
|
|
43
|
-
.option('--poll-timeout <seconds>', 'max seconds to wait for task completion', '
|
|
45
|
+
.option('--poll-timeout <seconds>', 'max seconds to wait for task completion', '1200')
|
|
44
46
|
.action(async (query, opts) => {
|
|
45
47
|
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
46
|
-
const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 ||
|
|
48
|
+
const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
|
|
47
49
|
const code = await slides(query, {
|
|
48
50
|
json: opts.json,
|
|
49
51
|
verbose: opts.verbose,
|
|
50
52
|
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
51
|
-
pollTimeoutMs: Number.isNaN(pollTimeoutMs) ?
|
|
53
|
+
pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
|
|
52
54
|
});
|
|
53
55
|
process.exitCode = code;
|
|
54
56
|
// Defer exit so stderr can flush; reduces Node.js Windows assertion (UV_HANDLE_CLOSING)
|
package/src/slides.js
CHANGED
|
@@ -2,8 +2,8 @@ import { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE } from './search.js
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
4
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
5
|
-
const POLL_INTERVAL_MS =
|
|
6
|
-
const MAX_POLL_TIMEOUT_MS =
|
|
5
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
6
|
+
const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
|
|
7
7
|
|
|
8
8
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
9
9
|
const STATUS_LINE_PAD = 50;
|
|
@@ -35,6 +35,10 @@ function clearStatusLine() {
|
|
|
35
35
|
process.stderr.write(`\r${' '.repeat(STATUS_LINE_PAD)}\r`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function normalizeTaskStatus(status) {
|
|
39
|
+
return String(status || '').trim().toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
/**
|
|
39
43
|
* Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
|
|
40
44
|
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
@@ -76,7 +80,7 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
* Get task historical info. Returns { task_status, live_doc_url? } or throws.
|
|
83
|
+
* Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
|
|
80
84
|
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
81
85
|
*/
|
|
82
86
|
async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
@@ -111,8 +115,11 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
return {
|
|
114
|
-
task_status: payload.task_status,
|
|
118
|
+
task_status: payload.task_status ?? payload.status,
|
|
119
|
+
ppt_url: payload.ppt_url,
|
|
120
|
+
ppt_biz_id: payload.ppt_biz_id,
|
|
115
121
|
live_doc_url: payload.live_doc_url,
|
|
122
|
+
live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
|
|
116
123
|
};
|
|
117
124
|
}
|
|
118
125
|
|
|
@@ -158,19 +165,28 @@ export async function slides(query, options = {}) {
|
|
|
158
165
|
await sleep(pollIntervalMs);
|
|
159
166
|
|
|
160
167
|
const historical = await getTaskHistorical(apiKey, taskId, requestTimeoutMs, apiBase);
|
|
161
|
-
|
|
168
|
+
const normalizedStatus = normalizeTaskStatus(historical.task_status);
|
|
169
|
+
lastStatus = normalizedStatus || historical.task_status;
|
|
162
170
|
|
|
171
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
163
172
|
if (useLiveStatus) {
|
|
164
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
165
173
|
spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
|
|
166
174
|
writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed);
|
|
175
|
+
} else if (!options.verbose && !options.json) {
|
|
176
|
+
process.stderr.write(` Generating... ${elapsed}s\n`);
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
const done = historical.task_status === 'COMPLETED' || historical.task_status === 'SUCCESS';
|
|
179
|
+
const done = normalizedStatus === 'COMPLETED' || normalizedStatus === 'SUCCESS';
|
|
171
180
|
if (done) {
|
|
172
181
|
if (useLiveStatus) clearStatusLine();
|
|
173
|
-
const
|
|
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 =
|
|
186
|
+
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);
|
|
189
|
+
const url = pptUrl || liveDocUrl;
|
|
174
190
|
if (options.json) {
|
|
175
191
|
console.log(
|
|
176
192
|
JSON.stringify(
|
|
@@ -178,9 +194,11 @@ export async function slides(query, options = {}) {
|
|
|
178
194
|
status: 'ok',
|
|
179
195
|
data: {
|
|
180
196
|
task_id: taskId,
|
|
181
|
-
task_status: historical.task_status,
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
task_status: normalizedStatus || historical.task_status,
|
|
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,
|
|
184
202
|
ppt_business_id: createResult.ppt_business_id,
|
|
185
203
|
},
|
|
186
204
|
},
|
|
@@ -190,10 +208,13 @@ export async function slides(query, options = {}) {
|
|
|
190
208
|
);
|
|
191
209
|
} else {
|
|
192
210
|
if (url) {
|
|
193
|
-
|
|
211
|
+
if (pptUrl && !options.json) {
|
|
212
|
+
process.stderr.write('PPT ready. Open this link to preview:\n');
|
|
213
|
+
}
|
|
214
|
+
console.log(pptUrl || liveDocUrl);
|
|
194
215
|
} else {
|
|
195
216
|
if (useLiveStatus) clearStatusLine();
|
|
196
|
-
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');
|
|
197
218
|
return 1;
|
|
198
219
|
}
|
|
199
220
|
}
|
|
@@ -201,17 +222,16 @@ export async function slides(query, options = {}) {
|
|
|
201
222
|
}
|
|
202
223
|
|
|
203
224
|
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
(historical.task_status !== 'RUNNING' && historical.task_status !== 'PENDING')
|
|
225
|
+
normalizedStatus === 'FAILED' ||
|
|
226
|
+
normalizedStatus === 'ERROR'
|
|
207
227
|
) {
|
|
208
228
|
if (useLiveStatus) clearStatusLine();
|
|
209
|
-
console.error(`Error: Task finished with status: ${historical.task_status}`);
|
|
229
|
+
console.error(`Error: Task finished with status: ${normalizedStatus || historical.task_status}`);
|
|
210
230
|
return 1;
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
if (options.verbose) {
|
|
214
|
-
process.stderr.write(` Status: ${historical.task_status}\n`);
|
|
234
|
+
process.stderr.write(` Status: ${normalizedStatus || historical.task_status || 'UNKNOWN'}\n`);
|
|
215
235
|
}
|
|
216
236
|
}
|
|
217
237
|
|
package/README.ja.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Felo AI
|
|
2
|
-
|
|
3
|
-
**何でも聞いて、AI で今の答えを。**
|
|
4
|
-
|
|
5
|
-
Felo AI はターミナル用 CLI と Claude Code スキルを提供し、日本語・英語・中国語(簡体・繁体)・韓国語に対応しています。
|
|
6
|
-
|
|
7
|
-
[]()
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 2つの機能
|
|
12
|
-
|
|
13
|
-
Felo AI のコア機能は **リアルタイム検索** と **PPT 生成** です。ターミナルの CLI で使うほか、Claude Code のスキルとしても利用でき、検索は自動で発動します。
|
|
14
|
-
|
|
15
|
-
### 機能1: リアルタイム検索
|
|
16
|
-
|
|
17
|
-
最新情報を検索し、AI がまとめた回答を取得します。天気、ニュース、価格、ドキュメント、技術動向など、「今」の情報が必要なときに最適です。
|
|
18
|
-
|
|
19
|
-
- **多言語対応**: お好みの言語で質問できます。
|
|
20
|
-
- **ターミナル**: `felo search "質問"`
|
|
21
|
-
- **Claude Code**: スキル導入後は自動で発動。手動では `/felo-ai 質問`
|
|
22
|
-
- **例**: `felo search "東京の天気"`、`felo search "React 19 new features" --verbose`
|
|
23
|
-
|
|
24
|
-
### 機能2: PPT 生成
|
|
25
|
-
|
|
26
|
-
テーマや説明を一文で指定すると、PPT を自動作成します。処理はクラウドで実行され、完了後に**オンライン文書リンク**が返り、ブラウザで開いて確認・編集できます。
|
|
27
|
-
|
|
28
|
-
- **ターミナル**: `felo slides "テーマや説明"`
|
|
29
|
-
- **例**: `felo slides "Felo 製品紹介、3枚"`、`felo slides "Introduction to React" --poll-timeout 300`
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## インストールと設定
|
|
34
|
-
|
|
35
|
-
### CLI のインストール
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install -g felo-ai
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
インストールせずに実行: `npx felo-ai search "東京の天気"`
|
|
42
|
-
インストール後のコマンドは `felo` です。
|
|
43
|
-
|
|
44
|
-
### API キー設定
|
|
45
|
-
|
|
46
|
-
推奨(永続保存):
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
felo config set FELO_API_KEY your-api-key-here
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
または環境変数: `export FELO_API_KEY="your-api-key-here"`(Linux/macOS)、`$env:FELO_API_KEY="your-api-key-here"`(Windows PowerShell)。
|
|
53
|
-
|
|
54
|
-
API キーは [felo.ai](https://felo.ai)(設定 → API Keys)で取得してください。
|
|
55
|
-
|
|
56
|
-
### コマンド一覧
|
|
57
|
-
|
|
58
|
-
| コマンド | 説明 |
|
|
59
|
-
|----------|------|
|
|
60
|
-
| `felo search "<query>"` | リアルタイム検索 |
|
|
61
|
-
| `felo slides "<prompt>"` | PPT 生成 |
|
|
62
|
-
| `felo config set FELO_API_KEY <key>` | API キー保存 |
|
|
63
|
-
| `felo config get/list/path/unset` | 設定の参照/一覧/パス/削除 |
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Claude Code スキル
|
|
68
|
-
|
|
69
|
-
スキルのインストール:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx @claude/skills add felo-ai
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`FELO_API_KEY` を設定後、「東京の今日の天気」「React 19 の新機能」などの質問で検索が自動発動します。PPT 生成は現状ターミナルの `felo slides` のみ対応です。
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## リンク
|
|
80
|
-
|
|
81
|
-
- [Felo オープンプラットフォーム](https://openapi.felo.ai/docs/) — API キー取得
|
|
82
|
-
- [API ドキュメント](https://openapi.felo.ai/docs/api-reference/v2/chat.html)
|
|
83
|
-
- [その他の例](./docs/EXAMPLES.md) | [FAQ](./docs/FAQ.md) | [GitHub Issues](https://github.com/Felo-Inc/felo-skills/issues)
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
**他の言語 / Other languages:** [简体中文](README.zh-CN.md) | [English](README.en.md) | [한국어](README.ko.md) | [繁體中文](README.zh-TW.md)
|
package/README.ko.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Felo AI
|
|
2
|
-
|
|
3
|
-
**무엇이든 물어보고, AI로 최신 답을 받으세요.**
|
|
4
|
-
|
|
5
|
-
Felo AI는 터미널 CLI와 Claude Code 스킬을 제공하며, 한국어·영어·중국어(간체·번체)·일본어를 지원합니다.
|
|
6
|
-
|
|
7
|
-
[]()
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 두 가지 핵심 기능
|
|
12
|
-
|
|
13
|
-
Felo AI의 핵심 기능은 **실시간 검색**과 **PPT 생성**입니다. 터미널 CLI로 사용하거나 Claude Code 스킬로 사용할 수 있으며, 검색은 자동으로 트리거됩니다.
|
|
14
|
-
|
|
15
|
-
### 기능 1: 실시간 검색
|
|
16
|
-
|
|
17
|
-
최신 정보를 검색하고 AI가 정리한 답변을 받습니다. 날씨, 뉴스, 가격, 문서, 기술 동향 등 「지금」 정보가 필요할 때 적합합니다.
|
|
18
|
-
|
|
19
|
-
- **다국어 지원**: 원하는 언어로 질문하면 됩니다.
|
|
20
|
-
- **터미널**: `felo search "질문"`
|
|
21
|
-
- **Claude Code**: 스킬 설치 후 자동 트리거, 또는 `/felo-ai 질문` 입력
|
|
22
|
-
- **예시**: `felo search "서울 날씨"`, `felo search "React 19 new features" --verbose`
|
|
23
|
-
|
|
24
|
-
### 기능 2: PPT 생성
|
|
25
|
-
|
|
26
|
-
주제나 설명을 한 문장으로 주면 PPT를 자동 생성합니다. 작업은 클라우드에서 실행되며, 완료 후 **온라인 문서 링크**를 반환해 브라우저에서 열어 확인·편집할 수 있습니다.
|
|
27
|
-
|
|
28
|
-
- **터미널**: `felo slides "주제 또는 설명"`
|
|
29
|
-
- **예시**: `felo slides "Felo 제품 소개, 3장"`, `felo slides "Introduction to React" --poll-timeout 300`
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## 설치 및 설정
|
|
34
|
-
|
|
35
|
-
### CLI 설치
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install -g felo-ai
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
설치 없이 실행: `npx felo-ai search "서울 날씨"`
|
|
42
|
-
설치 후 사용하는 명령은 `felo` 입니다.
|
|
43
|
-
|
|
44
|
-
### API 키 설정
|
|
45
|
-
|
|
46
|
-
권장(영구 저장):
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
felo config set FELO_API_KEY your-api-key-here
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
또는 환경 변수: `export FELO_API_KEY="your-api-key-here"` (Linux/macOS), `$env:FELO_API_KEY="your-api-key-here"` (Windows PowerShell).
|
|
53
|
-
|
|
54
|
-
API 키는 [felo.ai](https://felo.ai)(설정 → API Keys)에서 발급받을 수 있습니다.
|
|
55
|
-
|
|
56
|
-
### 명령어 요약
|
|
57
|
-
|
|
58
|
-
| 명령 | 설명 |
|
|
59
|
-
|------|------|
|
|
60
|
-
| `felo search "<query>"` | 실시간 검색 |
|
|
61
|
-
| `felo slides "<prompt>"` | PPT 생성 |
|
|
62
|
-
| `felo config set FELO_API_KEY <key>` | API 키 저장 |
|
|
63
|
-
| `felo config get/list/path/unset` | 설정 조회/목록/경로/삭제 |
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Claude Code 스킬
|
|
68
|
-
|
|
69
|
-
스킬 설치:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx @claude/skills add felo-ai
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`FELO_API_KEY` 설정 후, Claude Code에서 「서울 오늘 날씨」「React 19 새 기능」 같은 질문을 하면 검색이 자동으로 실행됩니다. PPT 생성은 현재 터미널의 `felo slides` 로만 지원됩니다.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## 링크
|
|
80
|
-
|
|
81
|
-
- [Felo 오픈 플랫폼](https://openapi.felo.ai/docs/) — API 키 발급
|
|
82
|
-
- [API 문서](https://openapi.felo.ai/docs/api-reference/v2/chat.html)
|
|
83
|
-
- [더 많은 예시](./docs/EXAMPLES.md) | [FAQ](./docs/FAQ.md) | [GitHub Issues](https://github.com/Felo-Inc/felo-skills/issues)
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
**다른 언어 / Other languages:** [简体中文](README.zh-CN.md) | [English](README.en.md) | [日本語](README.ja.md) | [繁體中文](README.zh-TW.md)
|
package/README.zh-CN.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Felo AI
|
|
2
|
-
|
|
3
|
-
**随时提问,用 AI 获取当下最新答案。**
|
|
4
|
-
|
|
5
|
-
Felo AI 提供终端 CLI 与 Claude Code 技能,支持简体中文、英文、日文、韩文、繁体中文等多语言。
|
|
6
|
-
|
|
7
|
-
[]()
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 两大能力
|
|
12
|
-
|
|
13
|
-
Felo AI 提供两个核心能力:**实时搜索** 与 **PPT 生成**。可在终端用 CLI 使用,也可在 Claude Code 中通过技能使用(搜索支持自动触发)。
|
|
14
|
-
|
|
15
|
-
### 能力一:实时搜索
|
|
16
|
-
|
|
17
|
-
联网搜索最新信息,由 AI 整合成可直接使用的回答。适合查天气、新闻、价格、文档、技术动态等一切需要「当下」信息的场景。
|
|
18
|
-
|
|
19
|
-
- **支持多语言**:用你习惯的语言提问即可。
|
|
20
|
-
- **终端**:`felo search "你的问题"`
|
|
21
|
-
- **Claude Code**:安装技能后自动触发,或输入 `/felo-ai 你的问题`
|
|
22
|
-
- **示例**:`felo search "杭州明天天气"`、`felo search "React 19 new features" --verbose`
|
|
23
|
-
|
|
24
|
-
### 能力二:生成 PPT
|
|
25
|
-
|
|
26
|
-
根据一句描述或主题,自动生成 PPT。任务在云端执行,完成后返回**在线文档链接**,浏览器打开即可查看/编辑。
|
|
27
|
-
|
|
28
|
-
- **终端**:`felo slides "你的主题或描述"`
|
|
29
|
-
- **示例**:`felo slides "Felo 产品介绍,3 页"`、`felo slides "Introduction to React" --poll-timeout 300`
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## 安装与配置
|
|
34
|
-
|
|
35
|
-
### 安装 CLI
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install -g felo-ai
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
不安装直接运行:`npx felo-ai search "东京天气"`
|
|
42
|
-
安装后命令为 `felo`。
|
|
43
|
-
|
|
44
|
-
### 配置 API Key
|
|
45
|
-
|
|
46
|
-
推荐(持久保存):
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
felo config set FELO_API_KEY your-api-key-here
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
或设置环境变量:`export FELO_API_KEY="your-api-key-here"`(Linux/macOS),`$env:FELO_API_KEY="your-api-key-here"`(Windows PowerShell)。
|
|
53
|
-
|
|
54
|
-
API Key 请在 [felo.ai](https://felo.ai)(设置 → API Keys)获取。
|
|
55
|
-
|
|
56
|
-
### 命令一览
|
|
57
|
-
|
|
58
|
-
| 命令 | 说明 |
|
|
59
|
-
|------|------|
|
|
60
|
-
| `felo search "<query>"` | 实时搜索 |
|
|
61
|
-
| `felo slides "<prompt>"` | 生成 PPT |
|
|
62
|
-
| `felo config set FELO_API_KEY <key>` | 保存 API Key |
|
|
63
|
-
| `felo config get/list/path/unset` | 查看/列出/定位/删除配置 |
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Claude Code 技能
|
|
68
|
-
|
|
69
|
-
安装技能:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx @claude/skills add felo-ai
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
配置 `FELO_API_KEY` 后,在 Claude Code 中问「东京今天天气」「React 19 新特性」等问题会自动触发搜索。PPT 生成目前仅支持终端 `felo slides`。
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## 链接
|
|
80
|
-
|
|
81
|
-
- [Felo 开放平台](https://openapi.felo.ai/docs/) — 获取 API Key
|
|
82
|
-
- [API 文档](https://openapi.felo.ai/docs/api-reference/v2/chat.html)
|
|
83
|
-
- [更多示例](./docs/EXAMPLES.md) | [FAQ](./docs/FAQ.md) | [GitHub Issues](https://github.com/Felo-Inc/felo-skills/issues)
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
**其他语言 / Other languages:** [English](README.en.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [繁體中文](README.zh-TW.md)
|