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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/index.js +50 -13
  3. 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.3.0";
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 val = eq >= 0 ? a.slice(eq + 1) : (argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true");
266
- opts[rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = val;
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
- for (const [name, c] of Object.entries(COMMANDS)) lines.push(` ${name.padEnd(14)} ${c.summary}`);
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
- " tools Print a JSON tool manifest for AI agents (function-calling).",
291
- " help Show this help.",
292
- " version Print the CLI version.",
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> API key (overrides FITVETE_API_KEY).",
296
- " --number, -n Max results for search commands (1-25).",
297
- " --base <url> API base URL (overrides FITVETE_API_BASE).",
298
- " --pretty Human-readable table instead of JSON.",
299
- " --json Force JSON output (default).",
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.0",
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": {