scavio 0.38.0 → 0.39.1
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 +34 -0
- package/dist/cli.js +441 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -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` or `$ZDOTDIR/.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
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
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.1" : "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") {
|
|
162
|
+
const zdot = env.ZDOTDIR;
|
|
163
|
+
if (zdot && zdot !== home) {
|
|
164
|
+
return [{ kind: "posix", path: join2(zdot, ".zshrc"), display: join2(zdot, ".zshrc").replace(home, "~") }];
|
|
165
|
+
}
|
|
166
|
+
return [t("posix", ".zshrc")];
|
|
167
|
+
}
|
|
168
|
+
if (shell === "bash") {
|
|
169
|
+
return platform === "darwin" ? [t("posix", ".bashrc"), t("posix", ".bash_profile")] : [t("posix", ".bashrc")];
|
|
170
|
+
}
|
|
171
|
+
return [t("posix", ".profile")];
|
|
172
|
+
}
|
|
173
|
+
function assertSafeKey(key) {
|
|
174
|
+
if (!/^sk_[A-Za-z0-9_-]{16,128}$/.test(key)) throw new Error("Unexpected API key format; nothing was written.");
|
|
175
|
+
}
|
|
176
|
+
function lineFor(kind, key) {
|
|
177
|
+
return kind === "fish" ? `set -gx SCAVIO_API_KEY "${key}" ${MARKER}` : `export SCAVIO_API_KEY="${key}" ${MARKER}`;
|
|
178
|
+
}
|
|
179
|
+
function upsertLine(content, line) {
|
|
180
|
+
const lines = content.split("\n");
|
|
181
|
+
const kept = lines.filter((l) => !l.includes(MARKER));
|
|
182
|
+
while (kept.length && kept[kept.length - 1] === "") kept.pop();
|
|
183
|
+
return (kept.length ? kept.join("\n") + "\n" : "") + line + "\n";
|
|
184
|
+
}
|
|
185
|
+
function removeLine(content) {
|
|
186
|
+
return content.split("\n").filter((l) => !l.includes(MARKER)).join("\n");
|
|
187
|
+
}
|
|
188
|
+
function writeProfiles(key, targets) {
|
|
189
|
+
assertSafeKey(key);
|
|
190
|
+
const written = [];
|
|
191
|
+
for (const t of targets) {
|
|
192
|
+
mkdirSync2(dirname(t.path), { recursive: true });
|
|
193
|
+
const before = existsSync2(t.path) ? readFileSync2(t.path, "utf8") : "";
|
|
194
|
+
writeFileSync2(t.path, upsertLine(before, lineFor(t.kind, key)));
|
|
195
|
+
written.push(t.display);
|
|
196
|
+
}
|
|
197
|
+
return written;
|
|
198
|
+
}
|
|
199
|
+
function removeFromProfiles(targets) {
|
|
200
|
+
const changed = [];
|
|
201
|
+
for (const t of targets) {
|
|
202
|
+
if (!existsSync2(t.path)) continue;
|
|
203
|
+
const before = readFileSync2(t.path, "utf8");
|
|
204
|
+
if (!before.includes(MARKER)) continue;
|
|
205
|
+
writeFileSync2(t.path, removeLine(before));
|
|
206
|
+
changed.push(t.display);
|
|
207
|
+
}
|
|
208
|
+
return changed;
|
|
209
|
+
}
|
|
210
|
+
function setWindowsUserEnv(key) {
|
|
211
|
+
if (key !== null) assertSafeKey(key);
|
|
212
|
+
const r = key === null ? spawnSync("reg", ["delete", "HKCU\\Environment", "/v", "SCAVIO_API_KEY", "/f"], { stdio: "ignore" }) : spawnSync("setx", ["SCAVIO_API_KEY", key], { stdio: "ignore" });
|
|
213
|
+
return r.status === 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/cli/index.ts
|
|
217
|
+
var KEYS_URL = "https://dashboard.scavio.dev/api-keys";
|
|
218
|
+
var UA = `scavio-cli/${SDK_VERSION}`;
|
|
219
|
+
var AGENT_WAIT_MS = 6e4;
|
|
220
|
+
var EXIT = { ok: 0, error: 1, pending: 2, denied: 3 };
|
|
221
|
+
var stdio = { log: (s) => process.stdout.write(s + "\n"), err: (s) => process.stderr.write(s + "\n") };
|
|
222
|
+
var HELP = `scavio ${SDK_VERSION} - sign this machine in to the Scavio API
|
|
223
|
+
|
|
224
|
+
Usage:
|
|
225
|
+
npx -y scavio@latest login Open a sign-in link; saves an API key for this machine
|
|
226
|
+
npx -y scavio@latest login --wait Finish a login started by an agent (exit 2 = still waiting)
|
|
227
|
+
npx -y scavio@latest status Check that the saved key works
|
|
228
|
+
npx -y scavio@latest logout Remove the saved key from this machine
|
|
229
|
+
|
|
230
|
+
Options:
|
|
231
|
+
--force Log in even if SCAVIO_API_KEY already works
|
|
232
|
+
--no-profile Do not add SCAVIO_API_KEY to your shell startup file
|
|
233
|
+
--no-browser Print the link without opening a browser
|
|
234
|
+
--timeout <s> How long --wait waits (default 60)
|
|
235
|
+
|
|
236
|
+
The key is never printed. Manage or revoke keys at ${KEYS_URL}`;
|
|
237
|
+
function tildify(p) {
|
|
238
|
+
const h = homedir3();
|
|
239
|
+
return p.startsWith(h) ? "~" + p.slice(h.length) : p;
|
|
240
|
+
}
|
|
241
|
+
function openBrowser(url) {
|
|
242
|
+
if (process.env.SSH_CONNECTION || process.env.CI) return;
|
|
243
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return;
|
|
244
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
245
|
+
try {
|
|
246
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
247
|
+
child.on("error", () => {
|
|
248
|
+
});
|
|
249
|
+
child.unref();
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function flag(args, name) {
|
|
254
|
+
return args.includes(name);
|
|
255
|
+
}
|
|
256
|
+
function option(args, name) {
|
|
257
|
+
const i = args.indexOf(name);
|
|
258
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
259
|
+
}
|
|
260
|
+
async function finish(result, dir, args, baseUrl, out) {
|
|
261
|
+
switch (result.kind) {
|
|
262
|
+
case "pending": {
|
|
263
|
+
const p = readPending(dir);
|
|
264
|
+
out.log("pending: not approved yet.");
|
|
265
|
+
if (p) out.log(`Open ${p.verification_uri_complete} and approve code ${p.user_code}.`);
|
|
266
|
+
out.log("Then run again: npx -y scavio@latest login --wait");
|
|
267
|
+
return EXIT.pending;
|
|
268
|
+
}
|
|
269
|
+
case "denied":
|
|
270
|
+
clearPending(dir);
|
|
271
|
+
out.err("The login was denied on the approval page. Nothing was saved.");
|
|
272
|
+
return EXIT.denied;
|
|
273
|
+
case "expired":
|
|
274
|
+
clearPending(dir);
|
|
275
|
+
out.err("The login code expired. Run: npx -y scavio@latest login");
|
|
276
|
+
return EXIT.error;
|
|
277
|
+
case "error":
|
|
278
|
+
clearPending(dir);
|
|
279
|
+
out.err(result.message);
|
|
280
|
+
return EXIT.error;
|
|
281
|
+
case "approved":
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
clearPending(dir);
|
|
285
|
+
let credPath;
|
|
286
|
+
try {
|
|
287
|
+
credPath = saveCredentials(dir, { api_key: result.apiKey, key_name: result.keyName, saved_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
288
|
+
} catch (e) {
|
|
289
|
+
out.err(`Approved, but the key could not be saved (${e.message}).`);
|
|
290
|
+
out.err(`Copy the key "${result.keyName}" from ${KEYS_URL} instead.`);
|
|
291
|
+
return EXIT.error;
|
|
292
|
+
}
|
|
293
|
+
const places = [];
|
|
294
|
+
if (!flag(args, "--no-profile")) {
|
|
295
|
+
try {
|
|
296
|
+
if (process.platform === "win32") {
|
|
297
|
+
if (setWindowsUserEnv(result.apiKey)) places.push("your Windows user environment");
|
|
298
|
+
} else {
|
|
299
|
+
places.push(...writeProfiles(result.apiKey, profileTargets()));
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
out.err(`Could not update the shell startup file (${e.message}).`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
out.log(`Logged in. API key "${result.keyName}" saved (it is not shown).`);
|
|
306
|
+
out.log(` Saved to: ${tildify(credPath)}`);
|
|
307
|
+
if (places.length) out.log(` SCAVIO_API_KEY added to: ${places.join(", ")}`);
|
|
308
|
+
const check = await checkKey(baseUrl, result.apiKey, UA);
|
|
309
|
+
if (check.ok) {
|
|
310
|
+
out.log(` Key works${check.plan ? `: ${check.plan} plan` : ""}${typeof check.credits === "number" ? `, ${check.credits} credits` : ""}.`);
|
|
311
|
+
}
|
|
312
|
+
if (places.length) {
|
|
313
|
+
out.log("Open a new terminal, or restart your AI agent, so it picks up SCAVIO_API_KEY.");
|
|
314
|
+
} else {
|
|
315
|
+
out.log("SCAVIO_API_KEY was not exported; read the key from the saved file or set it yourself.");
|
|
316
|
+
}
|
|
317
|
+
return EXIT.ok;
|
|
318
|
+
}
|
|
319
|
+
async function login(args, out) {
|
|
320
|
+
const dir = configDir();
|
|
321
|
+
const baseUrl = (process.env.SCAVIO_BASE_URL || BASE_URL).replace(/\/+$/, "");
|
|
322
|
+
const budget = Math.max(5, Number(option(args, "--timeout")) || AGENT_WAIT_MS / 1e3) * 1e3;
|
|
323
|
+
if (flag(args, "--wait")) {
|
|
324
|
+
const p = readPending(dir);
|
|
325
|
+
if (!p) {
|
|
326
|
+
out.err("No login in progress. Run: npx -y scavio@latest login");
|
|
327
|
+
return EXIT.error;
|
|
328
|
+
}
|
|
329
|
+
const r2 = await waitForApproval({
|
|
330
|
+
baseUrl: p.base_url,
|
|
331
|
+
deviceCode: p.device_code,
|
|
332
|
+
intervalSec: p.interval,
|
|
333
|
+
expiresAt: p.expires_at,
|
|
334
|
+
budgetMs: budget,
|
|
335
|
+
userAgent: UA
|
|
336
|
+
});
|
|
337
|
+
return finish(r2, dir, args, p.base_url, out);
|
|
338
|
+
}
|
|
339
|
+
if (!flag(args, "--force") && process.env.SCAVIO_API_KEY) {
|
|
340
|
+
const check = await checkKey(baseUrl, process.env.SCAVIO_API_KEY, UA);
|
|
341
|
+
if (check.ok) {
|
|
342
|
+
out.log("SCAVIO_API_KEY is already set and works. Nothing to do (use --force to log in again).");
|
|
343
|
+
return EXIT.ok;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
let code;
|
|
347
|
+
try {
|
|
348
|
+
code = await startLogin(baseUrl, { hostname: machineName(), os: process.platform, userAgent: UA });
|
|
349
|
+
} catch (e) {
|
|
350
|
+
out.err(e.message);
|
|
351
|
+
return EXIT.error;
|
|
352
|
+
}
|
|
353
|
+
const expiresAt = Date.now() + code.expires_in * 1e3;
|
|
354
|
+
savePending(dir, {
|
|
355
|
+
device_code: code.device_code,
|
|
356
|
+
user_code: code.user_code,
|
|
357
|
+
verification_uri_complete: code.verification_uri_complete,
|
|
358
|
+
interval: code.interval,
|
|
359
|
+
expires_at: expiresAt,
|
|
360
|
+
base_url: baseUrl
|
|
361
|
+
});
|
|
362
|
+
const minutes = Math.round(code.expires_in / 60);
|
|
363
|
+
out.log("To connect this machine to Scavio, open:");
|
|
364
|
+
out.log("");
|
|
365
|
+
out.log(` ${code.verification_uri_complete}`);
|
|
366
|
+
out.log("");
|
|
367
|
+
out.log(`and check that the page shows this code: ${code.user_code}`);
|
|
368
|
+
out.log(`Sign in or create a free account there. The link works for ${minutes} minutes.`);
|
|
369
|
+
if (!flag(args, "--no-browser")) openBrowser(code.verification_uri_complete);
|
|
370
|
+
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
371
|
+
if (!interactive) {
|
|
372
|
+
out.log("");
|
|
373
|
+
out.log("After approving, run: npx -y scavio@latest login --wait");
|
|
374
|
+
return EXIT.ok;
|
|
375
|
+
}
|
|
376
|
+
out.log("");
|
|
377
|
+
out.log("Waiting for approval...");
|
|
378
|
+
const r = await waitForApproval({
|
|
379
|
+
baseUrl,
|
|
380
|
+
deviceCode: code.device_code,
|
|
381
|
+
intervalSec: code.interval,
|
|
382
|
+
expiresAt,
|
|
383
|
+
budgetMs: code.expires_in * 1e3,
|
|
384
|
+
userAgent: UA
|
|
385
|
+
});
|
|
386
|
+
return finish(r, dir, args, baseUrl, out);
|
|
387
|
+
}
|
|
388
|
+
async function status(out) {
|
|
389
|
+
const baseUrl = (process.env.SCAVIO_BASE_URL || BASE_URL).replace(/\/+$/, "");
|
|
390
|
+
const saved = readCredentials(configDir());
|
|
391
|
+
const fromEnv = process.env.SCAVIO_API_KEY;
|
|
392
|
+
const key = fromEnv || saved?.api_key;
|
|
393
|
+
if (!key) {
|
|
394
|
+
out.log("Not logged in: SCAVIO_API_KEY is not set and no saved key was found. Run: npx -y scavio@latest login");
|
|
395
|
+
return EXIT.error;
|
|
396
|
+
}
|
|
397
|
+
const source = fromEnv ? "SCAVIO_API_KEY" : `saved key "${saved?.key_name}"`;
|
|
398
|
+
const check = await checkKey(baseUrl, key, UA);
|
|
399
|
+
if (!check.ok) {
|
|
400
|
+
out.log(`The key from ${source} does not work (HTTP ${check.status || "network error"}). Run: npx -y scavio@latest login --force`);
|
|
401
|
+
return EXIT.error;
|
|
402
|
+
}
|
|
403
|
+
out.log(`Logged in via ${source}${check.plan ? `: ${check.plan} plan` : ""}${typeof check.credits === "number" ? `, ${check.credits} credits` : ""}.`);
|
|
404
|
+
if (!fromEnv) out.log("SCAVIO_API_KEY is not set in this shell yet: open a new terminal or restart your agent.");
|
|
405
|
+
return EXIT.ok;
|
|
406
|
+
}
|
|
407
|
+
function logout(out) {
|
|
408
|
+
const dir = configDir();
|
|
409
|
+
const hadFile = clearCredentials(dir);
|
|
410
|
+
clearPending(dir);
|
|
411
|
+
const changed = process.platform === "win32" ? setWindowsUserEnv(null) ? ["Windows user environment"] : [] : removeFromProfiles(profileTargets());
|
|
412
|
+
if (!hadFile && changed.length === 0) {
|
|
413
|
+
out.log("Nothing to remove: no key saved by scavio login on this machine.");
|
|
414
|
+
return EXIT.ok;
|
|
415
|
+
}
|
|
416
|
+
out.log("Removed the saved key from this machine" + (changed.length ? ` and from ${changed.join(", ")}.` : "."));
|
|
417
|
+
out.log(`The key still exists on your account; revoke it at ${KEYS_URL} if you no longer need it.`);
|
|
418
|
+
return EXIT.ok;
|
|
419
|
+
}
|
|
420
|
+
async function main(argv = process.argv.slice(2), out = stdio) {
|
|
421
|
+
const [cmd, ...args] = argv;
|
|
422
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
423
|
+
out.log(SDK_VERSION);
|
|
424
|
+
return EXIT.ok;
|
|
425
|
+
}
|
|
426
|
+
if (cmd === "login") return login(args, out);
|
|
427
|
+
if (cmd === "status" || cmd === "whoami") return status(out);
|
|
428
|
+
if (cmd === "logout") return logout(out);
|
|
429
|
+
out.log(HELP);
|
|
430
|
+
return cmd && cmd !== "help" && cmd !== "--help" && cmd !== "-h" ? EXIT.error : EXIT.ok;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/cli/bin.ts
|
|
434
|
+
main().then(
|
|
435
|
+
(code) => process.exit(code),
|
|
436
|
+
(err) => {
|
|
437
|
+
process.stderr.write(`scavio: ${err?.message ?? String(err)}
|
|
438
|
+
`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
);
|
package/dist/index.cjs
CHANGED
|
@@ -265,7 +265,7 @@ function parseRetryAfter(header) {
|
|
|
265
265
|
var BASE_URL = "https://api.scavio.dev";
|
|
266
266
|
var DEFAULT_TIMEOUT = 3e4;
|
|
267
267
|
var DEFAULT_MAX_RETRIES = 2;
|
|
268
|
-
var SDK_VERSION = true ? "0.
|
|
268
|
+
var SDK_VERSION = true ? "0.39.1" : "0.0.0";
|
|
269
269
|
var USER_AGENT = `scavio-js/${SDK_VERSION}`;
|
|
270
270
|
function isServerRuntime() {
|
|
271
271
|
const g = globalThis;
|
package/dist/index.js
CHANGED
|
@@ -145,7 +145,7 @@ function parseRetryAfter(header) {
|
|
|
145
145
|
var BASE_URL = "https://api.scavio.dev";
|
|
146
146
|
var DEFAULT_TIMEOUT = 3e4;
|
|
147
147
|
var DEFAULT_MAX_RETRIES = 2;
|
|
148
|
-
var SDK_VERSION = true ? "0.
|
|
148
|
+
var SDK_VERSION = true ? "0.39.1" : "0.0.0";
|
|
149
149
|
var USER_AGENT = `scavio-js/${SDK_VERSION}`;
|
|
150
150
|
function isServerRuntime() {
|
|
151
151
|
const g = globalThis;
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scavio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "TypeScript SDK for the Scavio API — real-time structured web data across 61 platforms on one API key: Google SERP, ChatGPT, Gemini, Amazon, Walmart, Costco, Nordstrom, Nordstrom Rack, 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, HEB, Kroger, Mercado Libre, eBay, Zillow, Redfin, Realtor.com, Rightmove, Booking, Airbnb, Yelp, Trustpilot, Indeed, Glassdoor, G2, App Store, Google Play, SEC EDGAR, Google Ads and Meta Ad Library, YouTube, TikTok, Instagram, X, LinkedIn, Reddit and more, plus extract() to read any URL as clean Markdown and extract.map/crawl/batch to map, crawl and batch-extract whole websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"scavio": "./dist/cli.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"import": {
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
"LICENSE"
|
|
25
28
|
],
|
|
26
29
|
"scripts": {
|
|
27
|
-
"build": "tsup",
|
|
30
|
+
"build": "rm -rf dist && tsup",
|
|
28
31
|
"dev": "tsup --watch",
|
|
29
32
|
"test": "vitest run",
|
|
30
33
|
"test:watch": "vitest",
|