scavio 0.37.0 → 0.39.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Scavio
2
2
 
3
3
  TypeScript SDK for the [Scavio API](https://scavio.dev?utm_source=npm-scavio-js&utm_medium=package&utm_campaign=readme) — real-time structured web
4
- data across 60 platforms on one API key, plus `extract()` to read
4
+ data across 61 platforms on one API key, plus `extract()` to read
5
5
  any URL as clean Markdown, and `extract.map()` / `extract.crawl()` /
6
6
  `extract.batch()` to map, crawl and batch-extract whole websites.
7
7
 
@@ -9,7 +9,7 @@ Structured JSON in, structured JSON out. No per-site parsers to maintain.
9
9
 
10
10
  - **Search and SERP** — Google (organic, news, maps, shopping, flights, hotels, trends, AI Mode)
11
11
  - **AI assistants** — ChatGPT and Gemini answers with the sources they cite and the products they recommend
12
- - **E-commerce** — Amazon, Walmart, Costco, Lowe's, Sam's Club, Canadian Tire, Dollar General, Sephora, Ulta Beauty, ALDI, Micro Center, Walgreens, CVS, Whole Foods, Harbor Freight, PetSmart, Petco, iHerb, Chewy, Menards, Nordstrom, Mercado Libre, eBay, Target, Home Depot, TikTok Shop
12
+ - **E-commerce** — Amazon, Walmart, Costco, Lowe's, Sam's Club, Canadian Tire, Dollar General, Sephora, Ulta Beauty, ALDI, Micro Center, Walgreens, CVS, Whole Foods, Harbor Freight, PetSmart, Petco, iHerb, Chewy, Menards, Nordstrom, Nordstrom Rack, Mercado Libre, eBay, Target, Home Depot, TikTok Shop
13
13
  - **Real estate and travel** — Zillow, Redfin, Realtor.com, Rightmove, Booking, Airbnb, Tripadvisor
14
14
  - **Reviews and local** — Trustpilot, Yelp, G2, Capterra, Glassdoor, App Store, Google Play
15
15
  - **Jobs and companies** — Indeed, Glassdoor, SEC EDGAR, Companies House
@@ -24,6 +24,40 @@ Structured JSON in, structured JSON out. No per-site parsers to maintain.
24
24
  npm install scavio
25
25
  ```
26
26
 
27
+ ## Sign in without copying a key
28
+
29
+ ```bash
30
+ npx -y scavio@latest login
31
+ ```
32
+
33
+ Prints a link and a code and opens your browser. Sign in at dashboard.scavio.dev
34
+ (or create a free account, 50 credits, no card), check that the page shows the
35
+ same code, and click Approve. The CLI saves an API key named after your machine
36
+ (`CLI - <hostname>`) and never prints it:
37
+
38
+ - `~/.config/scavio/credentials.json` (owner-only), and
39
+ - `SCAVIO_API_KEY` in your shell startup file (`~/.zshrc`, `~/.bashrc` and on
40
+ macOS `~/.bash_profile`, fish `conf.d/scavio.fish`, or the Windows user
41
+ environment). Skip this with `--no-profile`.
42
+
43
+ Logging in again on the same machine reuses that machine's key. Open a new
44
+ terminal afterwards; `new Scavio()` reads `SCAVIO_API_KEY`.
45
+
46
+ AI coding agents (Claude Code, Codex, Pi, Cursor and others) can run it for you.
47
+ When its output is not a terminal, `login` prints the link and exits at once so
48
+ the agent can show it to you; the agent then runs `login --wait`, which waits up
49
+ to 60 seconds per run.
50
+
51
+ | Command | What it does |
52
+ |---|---|
53
+ | `scavio login` | Start a login (in a terminal it also waits for approval) |
54
+ | `scavio login --wait` | Finish a login an agent started. Exit 0 logged in, 2 still pending (run again), 3 denied, 1 expired or error |
55
+ | `scavio status` | Check the saved or exported key works, without printing it |
56
+ | `scavio logout` | Remove the saved key and the shell line (revoke it at dashboard.scavio.dev/api-keys) |
57
+
58
+ Options: `--force` (log in even if `SCAVIO_API_KEY` already works),
59
+ `--no-profile`, `--no-browser`, `--timeout <seconds>` for `--wait`.
60
+
27
61
  ## Quick Start
28
62
 
29
63
  ```typescript
@@ -763,7 +797,51 @@ request timeout to `NORDSTROM_TIMEOUT` (120s), except `reviews`, which takes
763
797
  20-60s (up to about 100s at 200 reviews) and raises it to
764
798
  `NORDSTROM_REVIEWS_TIMEOUT` (150s). Neither ever lowers a longer client timeout.
765
799
  If the upstream page still fails after the API's own retries, the call is an
766
- unbilled 502.
800
+ unbilled 502. For nordstromrack.com use `client.nordstromRack` (next section).
801
+
802
+ ### [Nordstrom Rack](https://scavio.dev/docs/nordstromrack-search?utm_source=npm-scavio-js&utm_medium=package&utm_campaign=readme)
803
+
804
+ Eight endpoints, new in 0.38.0, nordstromrack.com (US site only), on
805
+ `client.nordstromRack`: a separate platform from `client.nordstrom` with the
806
+ same methods, options and credit costs (`availability` and `reviews` 10, the
807
+ rest 5).
808
+
809
+ - `category` takes Rack's `/shop` and `/clearance` paths: departments, deals,
810
+ new arrivals, Clear the Rack (`/clearance`, `/clearance/women/clothing`) and
811
+ flash events (`/shop/flash` lists the live events as subcategories,
812
+ `/shop/flash/event-7` opens one). There is no `/browse` on Rack.
813
+ - `brand` takes `/brands/<slug>` (`/brands/adidas`); Rack brand pages carry no
814
+ id, so the nordstrom.com `--<id>` form is a 404.
815
+ - Every product row carries `clearance_price_range`, and during Clear the Rack
816
+ a clearance item can carry an extra percent off on top:
817
+ `promotion_price_range` is the price after it and `promotion_percent_off`
818
+ the extra percent. All three are null when not set; no response carries
819
+ event start or end dates.
820
+ - `stores` and `availability` cover Rack stores only; pickup quantities are
821
+ chain-wide.
822
+ - `reviews` returns Rack's own review pool, separate from nordstrom.com's, so
823
+ the same style can show a different rating and review count on each.
824
+
825
+ ```ts
826
+ const clr = (await client.nordstromRack.category({ category: "/clearance/women", sort: "percent_off" })) as any;
827
+ for (const p of clr.data.products.slice(0, 5)) {
828
+ console.log(p.style_id, p.name, p.price, p.clearance_price_range, p.promotion_price_range, p.promotion_percent_off);
829
+ }
830
+
831
+ const events = (await client.nordstromRack.category({ category: "/shop/flash" })) as any;
832
+ console.log(events.data.category.subcategories.map((e: any) => e.path));
833
+
834
+ const item = await client.nordstromRack.product({ style_id: "8816500" });
835
+ const brand = await client.nordstromRack.brand({ brand: "/brands/adidas" });
836
+ const stock = await client.nordstromRack.availability({ style_id: "7737947" });
837
+ const near = await client.nordstromRack.stores({ zip: "10001" });
838
+ const rev = await client.nordstromRack.reviews({ style_id: "7737947", max_reviews: 50 });
839
+ ```
840
+
841
+ Every call raises the request timeout to `NORDSTROM_RACK_TIMEOUT` (120s),
842
+ except `reviews`, which takes 15-60s (up to about 100s at 200 reviews) and
843
+ raises it to `NORDSTROM_RACK_REVIEWS_TIMEOUT` (150s). Neither ever lowers a
844
+ longer client timeout.
767
845
 
768
846
  ### [Mercado Libre](https://scavio.dev/docs/mercadolibre-search?utm_source=npm-scavio-js&utm_medium=package&utm_campaign=readme)
769
847
 
package/dist/cli.js ADDED
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { spawn } from "child_process";
5
+ import { homedir as homedir3 } from "os";
6
+
7
+ // src/http.ts
8
+ var BASE_URL = "https://api.scavio.dev";
9
+ var SDK_VERSION = true ? "0.39.0" : "0.0.0";
10
+ var USER_AGENT = `scavio-js/${SDK_VERSION}`;
11
+
12
+ // src/cli/files.ts
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ function configDir(env = process.env, platform = process.platform, home = homedir()) {
17
+ if (platform === "win32") return join(env.APPDATA || join(home, "AppData", "Roaming"), "scavio");
18
+ return join(env.XDG_CONFIG_HOME || join(home, ".config"), "scavio");
19
+ }
20
+ function writeSecret(path, dir, data) {
21
+ mkdirSync(dir, { recursive: true, mode: 448 });
22
+ try {
23
+ chmodSync(dir, 448);
24
+ } catch {
25
+ }
26
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
27
+ try {
28
+ chmodSync(path, 384);
29
+ } catch {
30
+ }
31
+ }
32
+ function readJson(path) {
33
+ if (!existsSync(path)) return null;
34
+ try {
35
+ return JSON.parse(readFileSync(path, "utf8"));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ var credentialsPath = (dir) => join(dir, "credentials.json");
41
+ var pendingPath = (dir) => join(dir, "pending-login.json");
42
+ function saveCredentials(dir, c) {
43
+ const p = credentialsPath(dir);
44
+ writeSecret(p, dir, c);
45
+ return p;
46
+ }
47
+ function readCredentials(dir) {
48
+ const c = readJson(credentialsPath(dir));
49
+ return c && typeof c.api_key === "string" ? c : null;
50
+ }
51
+ function savePending(dir, p) {
52
+ writeSecret(pendingPath(dir), dir, p);
53
+ }
54
+ function readPending(dir) {
55
+ const p = readJson(pendingPath(dir));
56
+ return p && typeof p.device_code === "string" ? p : null;
57
+ }
58
+ function clearPending(dir) {
59
+ rmSync(pendingPath(dir), { force: true });
60
+ }
61
+ function clearCredentials(dir) {
62
+ const p = credentialsPath(dir);
63
+ const had = existsSync(p);
64
+ rmSync(p, { force: true });
65
+ return had;
66
+ }
67
+
68
+ // src/cli/login.ts
69
+ import { hostname as osHostname } from "os";
70
+ function machineName(raw = osHostname()) {
71
+ return raw.split(".")[0].slice(0, 64) || "unknown";
72
+ }
73
+ async function startLogin(baseUrl, client, f = fetch) {
74
+ const res = await f(`${baseUrl}/api/v1/auth/device/code`, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json", "User-Agent": client.userAgent },
77
+ body: JSON.stringify({ hostname: client.hostname, os: client.os })
78
+ });
79
+ const body = await res.json().catch(() => ({}));
80
+ if (!res.ok || typeof body.device_code !== "string") {
81
+ throw new Error(body.error_description || `Could not start login (HTTP ${res.status}).`);
82
+ }
83
+ return body;
84
+ }
85
+ async function waitForApproval(opts) {
86
+ const f = opts.fetch ?? fetch;
87
+ const now = opts.now ?? Date.now;
88
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
89
+ const deadline = Math.min(now() + opts.budgetMs, opts.expiresAt);
90
+ let interval = Math.max(1, opts.intervalSec) * 1e3;
91
+ let netFailures = 0;
92
+ for (; ; ) {
93
+ if (now() >= opts.expiresAt) return { kind: "expired" };
94
+ let res;
95
+ let body;
96
+ try {
97
+ res = await f(`${opts.baseUrl}/api/v1/auth/device/token`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json", "User-Agent": opts.userAgent },
100
+ body: JSON.stringify({ device_code: opts.deviceCode })
101
+ });
102
+ body = await res.json().catch(() => ({}));
103
+ netFailures = 0;
104
+ } catch {
105
+ netFailures++;
106
+ const backoff2 = Math.min(interval * 2 ** netFailures, 3e4);
107
+ if (now() + backoff2 >= deadline) {
108
+ return now() >= opts.expiresAt ? { kind: "expired" } : { kind: "pending" };
109
+ }
110
+ await sleep(backoff2);
111
+ continue;
112
+ }
113
+ if (res.ok && typeof body.access_token === "string") {
114
+ return { kind: "approved", apiKey: body.access_token, keyName: body.key_name || "CLI" };
115
+ }
116
+ switch (body.error) {
117
+ case "authorization_pending":
118
+ break;
119
+ case "slow_down":
120
+ interval += 5e3;
121
+ break;
122
+ case "access_denied":
123
+ return { kind: "denied" };
124
+ case "expired_token":
125
+ return { kind: "expired" };
126
+ case "invalid_grant":
127
+ return { kind: "error", message: "This login was already used or is not valid. Run scavio login again." };
128
+ default:
129
+ return { kind: "error", message: `Login failed (HTTP ${res.status}${body.error ? `, ${body.error}` : ""}).` };
130
+ }
131
+ if (now() + interval >= deadline) {
132
+ return now() + interval >= opts.expiresAt ? { kind: "expired" } : { kind: "pending" };
133
+ }
134
+ await sleep(interval);
135
+ }
136
+ }
137
+ async function checkKey(baseUrl, apiKey, userAgent, f = fetch) {
138
+ try {
139
+ const res = await f(`${baseUrl}/api/v1/usage`, {
140
+ headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent }
141
+ });
142
+ if (!res.ok) return { ok: false, status: res.status };
143
+ const b = await res.json().catch(() => ({}));
144
+ return { ok: true, plan: b.plan, credits: typeof b.credit_balance === "number" ? b.credit_balance : void 0 };
145
+ } catch {
146
+ return { ok: false, status: 0 };
147
+ }
148
+ }
149
+
150
+ // src/cli/profile.ts
151
+ import { spawnSync } from "child_process";
152
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
153
+ import { homedir as homedir2 } from "os";
154
+ import { basename, dirname, join as join2 } from "path";
155
+ var MARKER = "# added by scavio login";
156
+ function profileTargets(env = process.env, platform = process.platform, home = homedir2()) {
157
+ if (platform === "win32") return [];
158
+ const shell = basename(env.SHELL || "");
159
+ const t = (kind, rel) => ({ kind, path: join2(home, rel), display: `~/${rel}` });
160
+ if (shell === "fish") return [t("fish", ".config/fish/conf.d/scavio.fish")];
161
+ if (shell === "zsh") return [t("posix", ".zshrc")];
162
+ if (shell === "bash") {
163
+ return platform === "darwin" ? [t("posix", ".bashrc"), t("posix", ".bash_profile")] : [t("posix", ".bashrc")];
164
+ }
165
+ return [t("posix", ".profile")];
166
+ }
167
+ function assertSafeKey(key) {
168
+ if (!/^sk_[A-Za-z0-9_-]{16,128}$/.test(key)) throw new Error("Unexpected API key format; nothing was written.");
169
+ }
170
+ function lineFor(kind, key) {
171
+ return kind === "fish" ? `set -gx SCAVIO_API_KEY "${key}" ${MARKER}` : `export SCAVIO_API_KEY="${key}" ${MARKER}`;
172
+ }
173
+ function upsertLine(content, line) {
174
+ const lines = content.split("\n");
175
+ const kept = lines.filter((l) => !l.includes(MARKER));
176
+ while (kept.length && kept[kept.length - 1] === "") kept.pop();
177
+ return (kept.length ? kept.join("\n") + "\n" : "") + line + "\n";
178
+ }
179
+ function removeLine(content) {
180
+ return content.split("\n").filter((l) => !l.includes(MARKER)).join("\n");
181
+ }
182
+ function writeProfiles(key, targets) {
183
+ assertSafeKey(key);
184
+ const written = [];
185
+ for (const t of targets) {
186
+ mkdirSync2(dirname(t.path), { recursive: true });
187
+ const before = existsSync2(t.path) ? readFileSync2(t.path, "utf8") : "";
188
+ writeFileSync2(t.path, upsertLine(before, lineFor(t.kind, key)));
189
+ written.push(t.display);
190
+ }
191
+ return written;
192
+ }
193
+ function removeFromProfiles(targets) {
194
+ const changed = [];
195
+ for (const t of targets) {
196
+ if (!existsSync2(t.path)) continue;
197
+ const before = readFileSync2(t.path, "utf8");
198
+ if (!before.includes(MARKER)) continue;
199
+ writeFileSync2(t.path, removeLine(before));
200
+ changed.push(t.display);
201
+ }
202
+ return changed;
203
+ }
204
+ function setWindowsUserEnv(key) {
205
+ if (key !== null) assertSafeKey(key);
206
+ const r = key === null ? spawnSync("reg", ["delete", "HKCU\\Environment", "/v", "SCAVIO_API_KEY", "/f"], { stdio: "ignore" }) : spawnSync("setx", ["SCAVIO_API_KEY", key], { stdio: "ignore" });
207
+ return r.status === 0;
208
+ }
209
+
210
+ // src/cli/index.ts
211
+ var KEYS_URL = "https://dashboard.scavio.dev/api-keys";
212
+ var UA = `scavio-cli/${SDK_VERSION}`;
213
+ var AGENT_WAIT_MS = 6e4;
214
+ var EXIT = { ok: 0, error: 1, pending: 2, denied: 3 };
215
+ var stdio = { log: (s) => process.stdout.write(s + "\n"), err: (s) => process.stderr.write(s + "\n") };
216
+ var HELP = `scavio ${SDK_VERSION} - sign this machine in to the Scavio API
217
+
218
+ Usage:
219
+ npx -y scavio@latest login Open a sign-in link; saves an API key for this machine
220
+ npx -y scavio@latest login --wait Finish a login started by an agent (exit 2 = still waiting)
221
+ npx -y scavio@latest status Check that the saved key works
222
+ npx -y scavio@latest logout Remove the saved key from this machine
223
+
224
+ Options:
225
+ --force Log in even if SCAVIO_API_KEY already works
226
+ --no-profile Do not add SCAVIO_API_KEY to your shell startup file
227
+ --no-browser Print the link without opening a browser
228
+ --timeout <s> How long --wait waits (default 60)
229
+
230
+ The key is never printed. Manage or revoke keys at ${KEYS_URL}`;
231
+ function tildify(p) {
232
+ const h = homedir3();
233
+ return p.startsWith(h) ? "~" + p.slice(h.length) : p;
234
+ }
235
+ function openBrowser(url) {
236
+ if (process.env.SSH_CONNECTION || process.env.CI) return;
237
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return;
238
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
239
+ try {
240
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
241
+ child.on("error", () => {
242
+ });
243
+ child.unref();
244
+ } catch {
245
+ }
246
+ }
247
+ function flag(args, name) {
248
+ return args.includes(name);
249
+ }
250
+ function option(args, name) {
251
+ const i = args.indexOf(name);
252
+ return i >= 0 ? args[i + 1] : void 0;
253
+ }
254
+ async function finish(result, dir, args, baseUrl, out) {
255
+ switch (result.kind) {
256
+ case "pending": {
257
+ const p = readPending(dir);
258
+ out.log("pending: not approved yet.");
259
+ if (p) out.log(`Open ${p.verification_uri_complete} and approve code ${p.user_code}.`);
260
+ out.log("Then run again: npx -y scavio@latest login --wait");
261
+ return EXIT.pending;
262
+ }
263
+ case "denied":
264
+ clearPending(dir);
265
+ out.err("The login was denied on the approval page. Nothing was saved.");
266
+ return EXIT.denied;
267
+ case "expired":
268
+ clearPending(dir);
269
+ out.err("The login code expired. Run: npx -y scavio@latest login");
270
+ return EXIT.error;
271
+ case "error":
272
+ clearPending(dir);
273
+ out.err(result.message);
274
+ return EXIT.error;
275
+ case "approved":
276
+ break;
277
+ }
278
+ clearPending(dir);
279
+ let credPath;
280
+ try {
281
+ credPath = saveCredentials(dir, { api_key: result.apiKey, key_name: result.keyName, saved_at: (/* @__PURE__ */ new Date()).toISOString() });
282
+ } catch (e) {
283
+ out.err(`Approved, but the key could not be saved (${e.message}).`);
284
+ out.err(`Copy the key "${result.keyName}" from ${KEYS_URL} instead.`);
285
+ return EXIT.error;
286
+ }
287
+ const places = [];
288
+ if (!flag(args, "--no-profile")) {
289
+ try {
290
+ if (process.platform === "win32") {
291
+ if (setWindowsUserEnv(result.apiKey)) places.push("your Windows user environment");
292
+ } else {
293
+ places.push(...writeProfiles(result.apiKey, profileTargets()));
294
+ }
295
+ } catch (e) {
296
+ out.err(`Could not update the shell startup file (${e.message}).`);
297
+ }
298
+ }
299
+ out.log(`Logged in. API key "${result.keyName}" saved (it is not shown).`);
300
+ out.log(` Saved to: ${tildify(credPath)}`);
301
+ if (places.length) out.log(` SCAVIO_API_KEY added to: ${places.join(", ")}`);
302
+ const check = await checkKey(baseUrl, result.apiKey, UA);
303
+ if (check.ok) {
304
+ out.log(` Key works${check.plan ? `: ${check.plan} plan` : ""}${typeof check.credits === "number" ? `, ${check.credits} credits` : ""}.`);
305
+ }
306
+ if (places.length) {
307
+ out.log("Open a new terminal, or restart your AI agent, so it picks up SCAVIO_API_KEY.");
308
+ } else {
309
+ out.log("SCAVIO_API_KEY was not exported; read the key from the saved file or set it yourself.");
310
+ }
311
+ return EXIT.ok;
312
+ }
313
+ async function login(args, out) {
314
+ const dir = configDir();
315
+ const baseUrl = (process.env.SCAVIO_BASE_URL || BASE_URL).replace(/\/+$/, "");
316
+ const budget = Math.max(5, Number(option(args, "--timeout")) || AGENT_WAIT_MS / 1e3) * 1e3;
317
+ if (flag(args, "--wait")) {
318
+ const p = readPending(dir);
319
+ if (!p) {
320
+ out.err("No login in progress. Run: npx -y scavio@latest login");
321
+ return EXIT.error;
322
+ }
323
+ const r2 = await waitForApproval({
324
+ baseUrl: p.base_url,
325
+ deviceCode: p.device_code,
326
+ intervalSec: p.interval,
327
+ expiresAt: p.expires_at,
328
+ budgetMs: budget,
329
+ userAgent: UA
330
+ });
331
+ return finish(r2, dir, args, p.base_url, out);
332
+ }
333
+ if (!flag(args, "--force") && process.env.SCAVIO_API_KEY) {
334
+ const check = await checkKey(baseUrl, process.env.SCAVIO_API_KEY, UA);
335
+ if (check.ok) {
336
+ out.log("SCAVIO_API_KEY is already set and works. Nothing to do (use --force to log in again).");
337
+ return EXIT.ok;
338
+ }
339
+ }
340
+ let code;
341
+ try {
342
+ code = await startLogin(baseUrl, { hostname: machineName(), os: process.platform, userAgent: UA });
343
+ } catch (e) {
344
+ out.err(e.message);
345
+ return EXIT.error;
346
+ }
347
+ const expiresAt = Date.now() + code.expires_in * 1e3;
348
+ savePending(dir, {
349
+ device_code: code.device_code,
350
+ user_code: code.user_code,
351
+ verification_uri_complete: code.verification_uri_complete,
352
+ interval: code.interval,
353
+ expires_at: expiresAt,
354
+ base_url: baseUrl
355
+ });
356
+ const minutes = Math.round(code.expires_in / 60);
357
+ out.log("To connect this machine to Scavio, open:");
358
+ out.log("");
359
+ out.log(` ${code.verification_uri_complete}`);
360
+ out.log("");
361
+ out.log(`and check that the page shows this code: ${code.user_code}`);
362
+ out.log(`Sign in or create a free account there. The link works for ${minutes} minutes.`);
363
+ if (!flag(args, "--no-browser")) openBrowser(code.verification_uri_complete);
364
+ const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
365
+ if (!interactive) {
366
+ out.log("");
367
+ out.log("After approving, run: npx -y scavio@latest login --wait");
368
+ return EXIT.ok;
369
+ }
370
+ out.log("");
371
+ out.log("Waiting for approval...");
372
+ const r = await waitForApproval({
373
+ baseUrl,
374
+ deviceCode: code.device_code,
375
+ intervalSec: code.interval,
376
+ expiresAt,
377
+ budgetMs: code.expires_in * 1e3,
378
+ userAgent: UA
379
+ });
380
+ return finish(r, dir, args, baseUrl, out);
381
+ }
382
+ async function status(out) {
383
+ const baseUrl = (process.env.SCAVIO_BASE_URL || BASE_URL).replace(/\/+$/, "");
384
+ const saved = readCredentials(configDir());
385
+ const fromEnv = process.env.SCAVIO_API_KEY;
386
+ const key = fromEnv || saved?.api_key;
387
+ if (!key) {
388
+ out.log("Not logged in: SCAVIO_API_KEY is not set and no saved key was found. Run: npx -y scavio@latest login");
389
+ return EXIT.error;
390
+ }
391
+ const source = fromEnv ? "SCAVIO_API_KEY" : `saved key "${saved?.key_name}"`;
392
+ const check = await checkKey(baseUrl, key, UA);
393
+ if (!check.ok) {
394
+ out.log(`The key from ${source} does not work (HTTP ${check.status || "network error"}). Run: npx -y scavio@latest login --force`);
395
+ return EXIT.error;
396
+ }
397
+ out.log(`Logged in via ${source}${check.plan ? `: ${check.plan} plan` : ""}${typeof check.credits === "number" ? `, ${check.credits} credits` : ""}.`);
398
+ if (!fromEnv) out.log("SCAVIO_API_KEY is not set in this shell yet: open a new terminal or restart your agent.");
399
+ return EXIT.ok;
400
+ }
401
+ function logout(out) {
402
+ const dir = configDir();
403
+ const hadFile = clearCredentials(dir);
404
+ clearPending(dir);
405
+ const changed = process.platform === "win32" ? setWindowsUserEnv(null) ? ["Windows user environment"] : [] : removeFromProfiles(profileTargets());
406
+ if (!hadFile && changed.length === 0) {
407
+ out.log("Nothing to remove: no key saved by scavio login on this machine.");
408
+ return EXIT.ok;
409
+ }
410
+ out.log("Removed the saved key from this machine" + (changed.length ? ` and from ${changed.join(", ")}.` : "."));
411
+ out.log(`The key still exists on your account; revoke it at ${KEYS_URL} if you no longer need it.`);
412
+ return EXIT.ok;
413
+ }
414
+ async function main(argv = process.argv.slice(2), out = stdio) {
415
+ const [cmd, ...args] = argv;
416
+ if (cmd === "--version" || cmd === "-v") {
417
+ out.log(SDK_VERSION);
418
+ return EXIT.ok;
419
+ }
420
+ if (cmd === "login") return login(args, out);
421
+ if (cmd === "status" || cmd === "whoami") return status(out);
422
+ if (cmd === "logout") return logout(out);
423
+ out.log(HELP);
424
+ return cmd && cmd !== "help" && cmd !== "--help" && cmd !== "-h" ? EXIT.error : EXIT.ok;
425
+ }
426
+
427
+ // src/cli/bin.ts
428
+ main().then(
429
+ (code) => process.exit(code),
430
+ (err) => {
431
+ process.stderr.write(`scavio: ${err?.message ?? String(err)}
432
+ `);
433
+ process.exit(1);
434
+ }
435
+ );