fitvete-food-cli 1.3.0 → 1.4.0
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.md +4 -0
- package/index.js +50 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -54,8 +54,12 @@ export FITVETE_API_KEY=fv_live_your_key
|
|
|
54
54
|
- `--key <key>` — API key (overrides `FITVETE_API_KEY`).
|
|
55
55
|
- `--number, -n <N>` — result count for search commands (1–25).
|
|
56
56
|
- `--base <url>` — API base URL (overrides `FITVETE_API_BASE`).
|
|
57
|
+
- `--timeout <ms>` — request timeout in milliseconds (overrides `FITVETE_API_TIMEOUT`; default `60000`, `none` disables it).
|
|
57
58
|
- `--pretty` — human-readable table instead of JSON.
|
|
58
59
|
- `--json` — force JSON (default).
|
|
60
|
+
- `--version, -v` — print the CLI version.
|
|
61
|
+
|
|
62
|
+
> `photo` and `label` images must be JPEG, PNG, or WebP and at most 5 MB.
|
|
59
63
|
|
|
60
64
|
### Exit codes
|
|
61
65
|
|
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
21
|
import { basename } from "node:path";
|
|
22
22
|
|
|
23
|
-
const VERSION = "1.
|
|
23
|
+
const VERSION = "1.4.0";
|
|
24
24
|
const DEFAULT_BASE = "https://auth.fitvete.com/functions/v1/food-api";
|
|
25
25
|
|
|
26
26
|
// ---- command registry (drives dispatch, --help, and the agent tool manifest) ----
|
|
@@ -180,6 +180,9 @@ const COMMANDS = {
|
|
|
180
180
|
{ name: "avoid", type: "string", description: "Comma-separated ingredients to avoid." },
|
|
181
181
|
{ name: "explain", type: "string", description: "Add a natural-language summary (--explain)." },
|
|
182
182
|
],
|
|
183
|
+
validate: (a, o) => {
|
|
184
|
+
if (!o.barcode && a.join(" ").trim() === "") fail(2, "score needs a food name or --barcode CODE");
|
|
185
|
+
},
|
|
183
186
|
path: () => "/v1/score",
|
|
184
187
|
jsonBody: (a, o) => ({
|
|
185
188
|
food: o.barcode ? { barcode: String(o.barcode) } : { name: a.join(" ") },
|
|
@@ -209,6 +212,9 @@ const COMMANDS = {
|
|
|
209
212
|
optionalArgs: true,
|
|
210
213
|
args: [{ name: "name", type: "string", required: false, description: "Food name (or use --barcode)." }],
|
|
211
214
|
options: [{ name: "barcode", type: "string", description: "Look up by UPC/EAN." }, { name: "number", type: "integer", description: "Max swaps, 1-10 (default 5)." }],
|
|
215
|
+
validate: (a, o) => {
|
|
216
|
+
if (!o.barcode && a.join(" ").trim() === "") fail(2, "alternatives needs a food name or --barcode CODE");
|
|
217
|
+
},
|
|
212
218
|
path: (a, o) => {
|
|
213
219
|
const p = new URLSearchParams();
|
|
214
220
|
if (o.barcode) p.set("barcode", String(o.barcode)); else p.set("name", a.join(" "));
|
|
@@ -238,11 +244,27 @@ function buildProfile(o) {
|
|
|
238
244
|
|
|
239
245
|
const enc = (s) => encodeURIComponent(s);
|
|
240
246
|
|
|
247
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
248
|
+
|
|
249
|
+
// Resolve the request timeout (ms) from --timeout, then FITVETE_API_TIMEOUT, then
|
|
250
|
+
// the default. `0` (or "none") disables the timeout for long-running AI calls.
|
|
251
|
+
function resolveTimeout(flag) {
|
|
252
|
+
const raw = flag != null ? flag : process.env.FITVETE_API_TIMEOUT;
|
|
253
|
+
if (raw == null || raw === "") return DEFAULT_TIMEOUT_MS;
|
|
254
|
+
if (String(raw).toLowerCase() === "none") return 0;
|
|
255
|
+
const n = Number(raw);
|
|
256
|
+
if (!Number.isFinite(n) || n < 0) fail(2, "--timeout must be a non-negative number of milliseconds (or 'none')");
|
|
257
|
+
return n;
|
|
258
|
+
}
|
|
259
|
+
|
|
241
260
|
function fail(code, msg) {
|
|
242
261
|
process.stderr.write(`fitvete-food: ${msg}\n`);
|
|
243
262
|
process.exit(code);
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
// Flags that never take a value (must not swallow the following positional).
|
|
266
|
+
const BOOLEAN_FLAGS = new Set(["pretty", "json", "help", "version", "explain"]);
|
|
267
|
+
|
|
246
268
|
// ---- argv parsing: positionals + --flags (--key, --base, --number, --pretty, --json) ----
|
|
247
269
|
function parseArgs(argv) {
|
|
248
270
|
const pos = [];
|
|
@@ -258,12 +280,17 @@ function parseArgs(argv) {
|
|
|
258
280
|
else if (a.startsWith("--key=")) opts.key = a.slice(6);
|
|
259
281
|
else if (a.startsWith("--base=")) opts.base = a.slice(7);
|
|
260
282
|
else if (a === "-h" || a === "--help") opts.help = true;
|
|
283
|
+
else if (a === "-v" || a === "--version") opts.version = true;
|
|
261
284
|
// Generic --flag value / --flag=value (recipe filters etc.), kebab -> camelCase.
|
|
262
285
|
else if (a.startsWith("--")) {
|
|
263
286
|
const eq = a.indexOf("=");
|
|
264
287
|
const rawKey = eq >= 0 ? a.slice(2, eq) : a.slice(2);
|
|
265
|
-
const
|
|
266
|
-
|
|
288
|
+
const key = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
289
|
+
// Boolean flags never consume the next token; everyone else takes --flag value.
|
|
290
|
+
const val = eq >= 0
|
|
291
|
+
? a.slice(eq + 1)
|
|
292
|
+
: (!BOOLEAN_FLAGS.has(key) && argv[i + 1] != null && !argv[i + 1].startsWith("--") ? argv[++i] : "true");
|
|
293
|
+
opts[key] = val;
|
|
267
294
|
}
|
|
268
295
|
else if (a.startsWith("-") && a !== "-") fail(2, `unknown option: ${a}`);
|
|
269
296
|
else pos.push(a);
|
|
@@ -285,18 +312,22 @@ function helpText() {
|
|
|
285
312
|
"",
|
|
286
313
|
"Commands:",
|
|
287
314
|
];
|
|
288
|
-
|
|
315
|
+
const names = [...Object.keys(COMMANDS), "tools", "help", "version"];
|
|
316
|
+
const w = Math.max(...names.map((n) => n.length));
|
|
317
|
+
for (const [name, c] of Object.entries(COMMANDS)) lines.push(` ${name.padEnd(w)} ${c.summary}`);
|
|
289
318
|
lines.push(
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
"
|
|
319
|
+
` ${"tools".padEnd(w)} Print a JSON tool manifest for AI agents (function-calling).`,
|
|
320
|
+
` ${"help".padEnd(w)} Show this help.`,
|
|
321
|
+
` ${"version".padEnd(w)} Print the CLI version.`,
|
|
293
322
|
"",
|
|
294
323
|
"Options:",
|
|
295
|
-
" --key <key>
|
|
296
|
-
" --number, -n
|
|
297
|
-
" --base <url>
|
|
298
|
-
" --
|
|
299
|
-
" --
|
|
324
|
+
" --key <key> API key (overrides FITVETE_API_KEY).",
|
|
325
|
+
" --number, -n Max results for search commands (1-25).",
|
|
326
|
+
" --base <url> API base URL (overrides FITVETE_API_BASE).",
|
|
327
|
+
" --timeout <ms> Request timeout in ms (default 60000; 'none' to disable).",
|
|
328
|
+
" --pretty Human-readable table instead of JSON.",
|
|
329
|
+
" --json Force JSON output (default).",
|
|
330
|
+
" --version, -v Print the CLI version.",
|
|
300
331
|
"",
|
|
301
332
|
"Auth: set FITVETE_API_KEY or pass --key. Get a free key at https://fitvete.com/api/",
|
|
302
333
|
"",
|
|
@@ -371,10 +402,15 @@ async function request(spec, args, opts) {
|
|
|
371
402
|
form.append("image", new Blob([bytes], { type: mimeType(filePath) }), basename(filePath));
|
|
372
403
|
init.body = form;
|
|
373
404
|
}
|
|
405
|
+
const timeoutMs = resolveTimeout(opts.timeout);
|
|
406
|
+
if (timeoutMs) init.signal = AbortSignal.timeout(timeoutMs);
|
|
374
407
|
let res;
|
|
375
408
|
try {
|
|
376
409
|
res = await fetch(base + path, init);
|
|
377
410
|
} catch (e) {
|
|
411
|
+
if (e && (e.name === "TimeoutError" || e.name === "AbortError")) {
|
|
412
|
+
fail(1, `request timed out after ${timeoutMs}ms (raise it with --timeout or FITVETE_API_TIMEOUT)`);
|
|
413
|
+
}
|
|
378
414
|
fail(1, `network error: ${e.message}`);
|
|
379
415
|
}
|
|
380
416
|
const text = await res.text();
|
|
@@ -426,13 +462,14 @@ function pretty(command, data) {
|
|
|
426
462
|
const { pos, opts } = parseArgs(process.argv.slice(2));
|
|
427
463
|
const command = pos.shift();
|
|
428
464
|
|
|
465
|
+
if (opts.version || command === "version") { process.stdout.write(VERSION + "\n"); process.exit(0); }
|
|
429
466
|
if (!command || command === "help" || opts.help) { process.stdout.write(helpText() + "\n"); process.exit(0); }
|
|
430
|
-
if (command === "version" || command === "--version" || command === "-v") { process.stdout.write(VERSION + "\n"); process.exit(0); }
|
|
431
467
|
if (command === "tools") { process.stdout.write(JSON.stringify(toolManifest(), null, 2) + "\n"); process.exit(0); }
|
|
432
468
|
|
|
433
469
|
const spec = COMMANDS[command];
|
|
434
470
|
if (!spec) fail(2, `unknown command: ${command}. Run 'fitvete-food help'.`);
|
|
435
471
|
if (pos.length === 0 && !spec.optionalArgs) fail(2, `usage: fitvete-food ${spec.usage}`);
|
|
472
|
+
if (spec.validate) spec.validate(pos, opts);
|
|
436
473
|
|
|
437
474
|
const data = await request(spec, pos, opts);
|
|
438
475
|
process.stdout.write((opts.pretty ? pretty(command, data) : JSON.stringify(data)) + "\n");
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fitvete-food-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "CLI for the FitVete Food & Nutrition API — clean JSON output for scripts and AI agents.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"fitvete-food": "index.js"
|
|
6
|
+
"fitvete-food": "index.js",
|
|
7
|
+
"fitvete-food-cli": "index.js"
|
|
7
8
|
},
|
|
8
9
|
"type": "module",
|
|
9
10
|
"engines": {
|