@earntori/cli 0.1.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 +8 -0
- package/dist/index.js +106 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @earntori/cli
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
npx @earntori/cli login # sign in with GitHub (device flow)
|
|
5
|
+
npx @earntori/cli status # balance + keys
|
|
6
|
+
npx @earntori/cli key --limit 5 --write env # mint a $5 capped AI key into .env
|
|
7
|
+
```
|
|
8
|
+
Your credits, in your tools, in one command. https://www.earntori.com
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @earntori/cli — tori login | status | key --limit N [--write env] | stats | logout
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync, unlinkSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".tori");
|
|
7
|
+
const CONFIG = join(CONFIG_DIR, "config.json");
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const cmd = args[0] ?? "help";
|
|
10
|
+
const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
|
|
11
|
+
const has = (name) => args.includes(`--${name}`);
|
|
12
|
+
const json = has("json");
|
|
13
|
+
function loadCfg() { try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
} }
|
|
19
|
+
function saveCfg(c) { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(CONFIG, JSON.stringify(c, null, 2), { mode: 0o600 }); }
|
|
20
|
+
function out(o, human) { console.log(json ? JSON.stringify(o, null, 2) : human); }
|
|
21
|
+
function die(msg) { console.error(json ? JSON.stringify({ error: msg }) : `\u2717 ${msg}`); process.exit(1); }
|
|
22
|
+
const usd = (c) => `$${(c / 100).toFixed(2)}`;
|
|
23
|
+
async function api(cfg, method, path, body) {
|
|
24
|
+
const r = await fetch(`${cfg.base}${path}`, { method, headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json", "User-Agent": "@earntori/cli/0.1.0" }, body: body ? JSON.stringify(body) : undefined });
|
|
25
|
+
const t = await r.text();
|
|
26
|
+
let j = null;
|
|
27
|
+
try {
|
|
28
|
+
j = t ? JSON.parse(t) : null;
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
if (!r.ok)
|
|
32
|
+
die(typeof j?.error === "string" ? j.error : `Tori API ${r.status}${t ? ": " + t.slice(0, 200) : ""}`);
|
|
33
|
+
return j;
|
|
34
|
+
}
|
|
35
|
+
async function login() {
|
|
36
|
+
const base = (flag("base") ?? "https://www.earntori.com").replace(/\/$/, "");
|
|
37
|
+
const conf = await (await fetch(`${base}/api/v1/auth/cli/config`)).json();
|
|
38
|
+
if (!conf.githubClientId)
|
|
39
|
+
die("GitHub sign-in is not configured on this Tori server");
|
|
40
|
+
const dc = await (await fetch("https://github.com/login/device/code", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ client_id: conf.githubClientId, scope: "read:user user:email" }) })).json();
|
|
41
|
+
console.log(`\nOpen ${dc.verification_uri} and enter code: ${dc.user_code}\n\nWaiting for you to authorize in the browser\u2026`);
|
|
42
|
+
const deadline = Date.now() + dc.expires_in * 1000;
|
|
43
|
+
let interval = (dc.interval ?? 5) * 1000;
|
|
44
|
+
let token;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
47
|
+
const t = await (await fetch("https://github.com/login/oauth/access_token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ client_id: conf.githubClientId, device_code: dc.device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }) })).json();
|
|
48
|
+
if (t.access_token) {
|
|
49
|
+
token = t.access_token;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (t.error === "slow_down")
|
|
53
|
+
interval += 5000;
|
|
54
|
+
else if (t.error && t.error !== "authorization_pending")
|
|
55
|
+
die(`GitHub: ${t.error}`);
|
|
56
|
+
}
|
|
57
|
+
if (!token)
|
|
58
|
+
die("Timed out waiting for GitHub authorization");
|
|
59
|
+
const s = await (await fetch(`${base}/api/v1/auth/cli/github`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_token: token }) })).json();
|
|
60
|
+
if (!s.token)
|
|
61
|
+
die(s.error ?? "Tori did not return a session");
|
|
62
|
+
saveCfg({ base, token: s.token, email: s.email, githubLogin: s.githubLogin });
|
|
63
|
+
out({ ok: true, email: s.email, githubLogin: s.githubLogin, base }, `\u2713 Signed in as ${s.githubLogin} (${s.email}) on ${base}`);
|
|
64
|
+
}
|
|
65
|
+
async function status() {
|
|
66
|
+
const cfg = loadCfg();
|
|
67
|
+
if (!cfg)
|
|
68
|
+
die("Not signed in \u2014 run: tori login");
|
|
69
|
+
const wallet = await api(cfg, "GET", "/api/v1/wallet");
|
|
70
|
+
const keys = await api(cfg, "GET", "/api/v1/redemptions/key").catch(() => ({ keys: [] }));
|
|
71
|
+
const list = Array.isArray(keys?.keys) ? keys.keys : [];
|
|
72
|
+
const avail = wallet?.availableCents ?? wallet?.balances?.availableCents;
|
|
73
|
+
out({ wallet, keys: list }, `Signed in as ${cfg.githubLogin ?? cfg.email}\nAvailable: ${typeof avail === "number" ? usd(avail) : JSON.stringify(wallet)}\nKeys: ${list.length === 0 ? "none" : ""}` + list.map((k) => `\n ${k.label ?? k.id} ${usd(k.usedCents ?? 0)} used of ${usd(k.limitCents ?? 0)} ${k.status ?? ""}`).join(""));
|
|
74
|
+
}
|
|
75
|
+
async function key() {
|
|
76
|
+
const cfg = loadCfg();
|
|
77
|
+
if (!cfg)
|
|
78
|
+
die("Not signed in \u2014 run: tori login");
|
|
79
|
+
const dollars = Number(flag("limit") ?? "0");
|
|
80
|
+
if (!(dollars >= 1))
|
|
81
|
+
die("Use --limit <dollars>, minimum 1");
|
|
82
|
+
const r = await api(cfg, "POST", "/api/v1/redemptions/key", { amountCents: Math.round(dollars * 100) });
|
|
83
|
+
const write = flag("write");
|
|
84
|
+
let note = "";
|
|
85
|
+
if (write === "env") {
|
|
86
|
+
const file = flag("file") ?? ".env";
|
|
87
|
+
appendFileSync(file, `\n# Tori AI key (${r.label}, cap ${usd(r.limitCents)})\nOPENAI_API_KEY=${r.key}\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\n`);
|
|
88
|
+
note = `\n\u2713 Wrote OPENAI_API_KEY and OPENAI_BASE_URL to ${file}`;
|
|
89
|
+
}
|
|
90
|
+
else if (write)
|
|
91
|
+
die(`--write ${write} is not supported yet; use --write env`);
|
|
92
|
+
out({ ...r, wrote: write === "env" ? (flag("file") ?? ".env") : null }, `\u2713 AI key minted \u2014 cap ${usd(r.limitCents)} \u2014 shown once:\n\n ${r.key}\n\nUse it anywhere the OpenAI API works: base URL https://openrouter.ai/api/v1, pick any major model.\n Cursor: Settings \u2192 Models \u2192 OpenAI API key + override base URL\n Claude Code / others: set OPENAI_API_KEY and OPENAI_BASE_URL${note}`);
|
|
93
|
+
}
|
|
94
|
+
function logout() { if (existsSync(CONFIG))
|
|
95
|
+
unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
|
|
96
|
+
function help() { console.log(`tori \u2014 Tori CLI\n\n tori login [--base URL] sign in with GitHub (device flow)\n tori status [--json] balance + your AI keys\n tori key --limit 5 [--write env] [--file .env] [--json]\n tori stats developer program stats (coming with the stats endpoint)\n tori logout\n`); }
|
|
97
|
+
(async () => {
|
|
98
|
+
switch (cmd) {
|
|
99
|
+
case "login": return login();
|
|
100
|
+
case "status": return status();
|
|
101
|
+
case "key": return key();
|
|
102
|
+
case "stats": return out({ pending: true }, "tori stats arrives with the developer stats endpoint (piece G). For now: https://www.earntori.com/developer");
|
|
103
|
+
case "logout": return logout();
|
|
104
|
+
default: return help();
|
|
105
|
+
}
|
|
106
|
+
})().catch((e) => die(e instanceof Error ? e.message : String(e)));
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@earntori/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tori CLI — sign in with GitHub, check your credits, mint a capped AI key into your tools.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "tori": "dist/index.js" },
|
|
8
|
+
"files": ["dist", "README.md"],
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"scripts": { "build": "tsc -p tsconfig.json && chmod +x dist/index.js", "typecheck": "tsc --noEmit -p tsconfig.json", "test": "echo no tests" },
|
|
11
|
+
"devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" },
|
|
12
|
+
"publishConfig": { "access": "public" }
|
|
13
|
+
}
|