@earntori/cli 0.2.2 → 0.2.4
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 +1 -1
- package/dist/index.js +123 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,4 +14,4 @@ npx @earntori/cli logout
|
|
|
14
14
|
|
|
15
15
|
**Earn AI credits by building securely in Claude Code and Cursor.** Tori checks the code your coding agent writes before you ship it, tells the agent how to fix what it finds, and rewards you in AI credits for building with it. Full guide: https://www.earntori.com/docs/mcp · issues: https://github.com/GAishZOEish/Tori-MCP/issues
|
|
16
16
|
|
|
17
|
-
Sending rewards to your own users? That's the Partner API: https://
|
|
17
|
+
Sending rewards to your own users? That's the Partner API: https://www.earntori.com/developers#quickstart
|
package/dist/index.js
CHANGED
|
@@ -4,26 +4,58 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync, unl
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve, relative, isAbsolute, dirname } from "node:path";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
7
|
+
import { createHash, createPublicKey, verify as edVerify } from "node:crypto";
|
|
8
8
|
const CONFIG_DIR = join(homedir(), ".tori");
|
|
9
9
|
const CONFIG = join(CONFIG_DIR, "config.json");
|
|
10
|
+
const SESSIONS_DIR = join(CONFIG_DIR, "sessions");
|
|
11
|
+
const hostOf = (base) => base.replace(/^https?:\/\//, "").replace(/[^a-z0-9.-]/gi, "_");
|
|
10
12
|
const args = process.argv.slice(2);
|
|
11
13
|
const cmd = args[0] ?? "help";
|
|
12
14
|
const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
|
|
13
15
|
const has = (name) => args.includes(`--${name}`);
|
|
14
16
|
const json = has("json");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/** Effective config: the global login, unless this repo pins a base in .tori/project.json — then that base with its own saved session. */
|
|
18
|
+
function projectBase() {
|
|
19
|
+
let dir = process.cwd();
|
|
20
|
+
for (let i = 0; i < 8; i++) {
|
|
21
|
+
try {
|
|
22
|
+
const j = JSON.parse(readFileSync(join(dir, ".tori", "project.json"), "utf8"));
|
|
23
|
+
if (typeof j?.base === "string")
|
|
24
|
+
return j.base.replace(/\/$/, "");
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
const up = dirname(dir);
|
|
28
|
+
if (up === dir)
|
|
29
|
+
break;
|
|
30
|
+
dir = up;
|
|
31
|
+
}
|
|
19
32
|
return null;
|
|
20
|
-
}
|
|
33
|
+
}
|
|
34
|
+
function loadCfg() {
|
|
35
|
+
let global = null;
|
|
36
|
+
try {
|
|
37
|
+
global = JSON.parse(readFileSync(CONFIG, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
const pinned = projectBase();
|
|
41
|
+
if (!pinned)
|
|
42
|
+
return global;
|
|
43
|
+
if (global && global.base === pinned)
|
|
44
|
+
return global;
|
|
45
|
+
try {
|
|
46
|
+
const sess = JSON.parse(readFileSync(join(SESSIONS_DIR, hostOf(pinned) + ".json"), "utf8"));
|
|
47
|
+
return { ...sess, base: pinned };
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
return null; // pinned to a base we have no session for
|
|
51
|
+
}
|
|
52
|
+
function saveSession(c) { mkdirSync(SESSIONS_DIR, { recursive: true }); writeFileSync(join(SESSIONS_DIR, hostOf(c.base) + ".json"), JSON.stringify(c, null, 2), { mode: 0o600 }); }
|
|
21
53
|
function saveCfg(c) { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(CONFIG, JSON.stringify(c, null, 2), { mode: 0o600 }); }
|
|
22
54
|
function out(o, human) { console.log(json ? JSON.stringify(o, null, 2) : human); }
|
|
23
55
|
function die(msg) { console.error(json ? JSON.stringify({ error: msg }) : `\u2717 ${msg}`); process.exit(1); }
|
|
24
56
|
const usd = (c) => `$${(c / 100).toFixed(2)}`;
|
|
25
57
|
async function api(cfg, method, path, body) {
|
|
26
|
-
const r = await fetch(`${cfg.base}${path}`, { method, headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json", "User-Agent": "@earntori/cli/0.2.
|
|
58
|
+
const r = await fetch(`${cfg.base}${path}`, { method, headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json", "User-Agent": "@earntori/cli/0.2.4" }, body: body ? JSON.stringify(body) : undefined });
|
|
27
59
|
const t = await r.text();
|
|
28
60
|
let j = null;
|
|
29
61
|
try {
|
|
@@ -62,6 +94,7 @@ async function login() {
|
|
|
62
94
|
if (!s.token)
|
|
63
95
|
die(s.error ?? "Tori did not return a session");
|
|
64
96
|
saveCfg({ base, token: s.token, email: s.email, githubLogin: s.githubLogin });
|
|
97
|
+
saveSession({ base, token: s.token, email: s.email, githubLogin: s.githubLogin });
|
|
65
98
|
out({ ok: true, email: s.email, githubLogin: s.githubLogin, base }, `\u2713 Signed in as ${s.githubLogin} (${s.email}) on ${base}`);
|
|
66
99
|
}
|
|
67
100
|
async function status() {
|
|
@@ -124,9 +157,12 @@ catch {
|
|
|
124
157
|
} }
|
|
125
158
|
function writeJson(p, o) { mkdirSync(dirname(resolve(p)), { recursive: true }); writeFileSync(p, JSON.stringify(o, null, 2) + "\n"); }
|
|
126
159
|
function init() {
|
|
160
|
+
const pin = flag("base");
|
|
161
|
+
if (pin)
|
|
162
|
+
writeJson(join(".tori", "project.json"), { base: pin.replace(/\/$/, "") });
|
|
127
163
|
const cfg = loadCfg();
|
|
128
164
|
if (!cfg)
|
|
129
|
-
die("Not signed in \u2014 run: tori login");
|
|
165
|
+
die(pin ? `Not signed in for ${pin} \u2014 run: tori login --base ${pin}` : "Not signed in \u2014 run: tori login");
|
|
130
166
|
const local = has("local");
|
|
131
167
|
const cliBin = resolve(process.argv[1] ?? "");
|
|
132
168
|
const mcpBin = resolve(dirname(cliBin), "..", "..", "mcp", "dist", "index.js");
|
|
@@ -150,7 +186,9 @@ function init() {
|
|
|
150
186
|
ensure("SessionStart", `${cli} check --reminder`);
|
|
151
187
|
writeJson(sfile, s);
|
|
152
188
|
wrote.push(sfile);
|
|
153
|
-
|
|
189
|
+
if (pin)
|
|
190
|
+
wrote.push(join(".tori", "project.json"));
|
|
191
|
+
out({ ok: true, wrote, local }, `\u2713 Tori installed in this project${local ? " (local builds)" : ""}${pin ? ` \u2014 pinned to ${pin}` : ""}\n` +
|
|
154
192
|
wrote.map((w) => ` ${w}`).join("\n") +
|
|
155
193
|
`\n\nClaude Code: restart the session (hooks and .mcp.json load at start). Cursor: reload the window.\nTori's checks now run when the agent finishes a turn; findings are shown to the agent before it stops.`);
|
|
156
194
|
}
|
|
@@ -199,11 +237,13 @@ function formatRun(r, n) {
|
|
|
199
237
|
L.push("", `Waived (${r.waived.length}, not blocking): ` + r.waived.map((w) => `${w.path}${w.line ? ":" + w.line : ""} ${w.rule_id} \u2014 ${w.reason ?? "no reason"}`).join("; "));
|
|
200
238
|
if (r.reward?.issued)
|
|
201
239
|
L.push("", `Tori rewarded you (${r.reward.reward}) \u2014 details in your Tori ledger.`);
|
|
240
|
+
if (r.receipt)
|
|
241
|
+
L.push(`Receipt: ${r.receipt.short}`);
|
|
202
242
|
return L.join("\n");
|
|
203
243
|
}
|
|
204
244
|
async function check() {
|
|
205
245
|
if (has("reminder")) {
|
|
206
|
-
console.log("Tori is installed in this project. Before finishing any work that touches webhooks, payments, banking, identity or third-party APIs, call the checks_run tool on the files you changed; after fixing a finding, call checks_resolve with the finding_id and path. Tori also runs automatically when you stop.");
|
|
246
|
+
console.log("Tori is installed in this project. Before finishing any work that touches webhooks, payments, banking, identity or third-party APIs, call the checks_run tool on the files you changed; after fixing a finding, call checks_resolve with the finding_id and path. Tori also runs automatically when you stop. Everything you do here is journaled — the ledger_query tool reads it back.");
|
|
207
247
|
return;
|
|
208
248
|
}
|
|
209
249
|
const hook = has("hook");
|
|
@@ -256,15 +296,87 @@ async function check() {
|
|
|
256
296
|
if (!r.clean)
|
|
257
297
|
process.exitCode = 1;
|
|
258
298
|
}
|
|
299
|
+
async function ledger() {
|
|
300
|
+
const cfg = loadCfg();
|
|
301
|
+
if (!cfg)
|
|
302
|
+
die("Not signed in \u2014 run: tori login");
|
|
303
|
+
const q = new URLSearchParams();
|
|
304
|
+
const proj = flag("project") ?? projectId();
|
|
305
|
+
if (proj !== "all")
|
|
306
|
+
q.set("project", proj);
|
|
307
|
+
if (flag("type"))
|
|
308
|
+
q.set("event_type", flag("type"));
|
|
309
|
+
if (flag("since"))
|
|
310
|
+
q.set("since", flag("since"));
|
|
311
|
+
if (flag("until"))
|
|
312
|
+
q.set("until", flag("until"));
|
|
313
|
+
if (flag("limit"))
|
|
314
|
+
q.set("limit", flag("limit"));
|
|
315
|
+
if (flag("before"))
|
|
316
|
+
q.set("before_seq", flag("before"));
|
|
317
|
+
const r = await api(cfg, "GET", `/api/v1/ledger/query?${q}`);
|
|
318
|
+
const line = (e) => {
|
|
319
|
+
const p = e.payload ?? {};
|
|
320
|
+
const bits = [];
|
|
321
|
+
if (p.rule_id)
|
|
322
|
+
bits.push(p.rule_id);
|
|
323
|
+
if (p.path)
|
|
324
|
+
bits.push(`${p.path}${p.line ? ":" + p.line : ""}`);
|
|
325
|
+
if (p.via)
|
|
326
|
+
bits.push(`via ${p.via}`);
|
|
327
|
+
if (p.reward)
|
|
328
|
+
bits.push(`${p.reward}${p.amount_cents ? " " + p.amount_cents + "\u00A2" : ""}`);
|
|
329
|
+
if (p.reason_code)
|
|
330
|
+
bits.push(`withheld: ${p.reason_code}`);
|
|
331
|
+
if (typeof p.clean === "boolean")
|
|
332
|
+
bits.push(`${p.open ?? 0} open, ${p.waived ?? 0} waived`);
|
|
333
|
+
if (p.harness)
|
|
334
|
+
bits.push(p.harness);
|
|
335
|
+
return `#${e.seq} ${String(e.ts).replace("T", " ").slice(0, 19)}Z ${e.event_type} [${e.actor_type}] ${bits.join(" \u00B7 ")} \u2014 ${e.receipt.short}`;
|
|
336
|
+
};
|
|
337
|
+
out(r, r.events.length ? r.events.map(line).join("\n") + (r.next_before_seq ? `\n(more: --before ${r.next_before_seq})` : "") : `No journal events for ${proj}.`);
|
|
338
|
+
}
|
|
339
|
+
async function receipt() {
|
|
340
|
+
const cfg = loadCfg();
|
|
341
|
+
if (!cfg)
|
|
342
|
+
die("Not signed in \u2014 run: tori login");
|
|
343
|
+
const id = args[1];
|
|
344
|
+
if (!id || id.startsWith("--"))
|
|
345
|
+
die("Usage: tori receipt <event_id|seq>");
|
|
346
|
+
const r = await api(cfg, "GET", `/api/v1/ledger/receipts/${encodeURIComponent(id)}`);
|
|
347
|
+
const k = await api(cfg, "GET", "/api/v1/ledger/receipt-key");
|
|
348
|
+
const recomputed = createHash("sha256").update(r.preimage, "utf8").digest("hex");
|
|
349
|
+
const hashOk = recomputed === r.hash;
|
|
350
|
+
let sigState = "UNSIGNED";
|
|
351
|
+
if (r.sig) {
|
|
352
|
+
if (!k.public_key_spki_b64)
|
|
353
|
+
sigState = "NO_KEY";
|
|
354
|
+
else {
|
|
355
|
+
try {
|
|
356
|
+
const pub = createPublicKey({ key: Buffer.from(k.public_key_spki_b64, "base64"), format: "der", type: "spki" });
|
|
357
|
+
sigState = edVerify(null, Buffer.from(r.signed, "utf8"), pub, Buffer.from(r.sig, "base64")) ? "VERIFIED" : "INVALID";
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
sigState = "INVALID";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const verdict = hashOk ? (sigState === "VERIFIED" ? "VERIFIED" : sigState) : "INVALID (hash mismatch)";
|
|
365
|
+
out({ ...r, recomputed_hash: recomputed, hash_ok: hashOk, signature: sigState, verdict }, `${verdict === "VERIFIED" ? "\u2713" : "\u2717"} ${verdict}\n#${r.seq} ${r.event_type} [${r.actor_type}] ${String(r.ts)}\nproject ${r.project_id}${r.ref_type ? ` \u00B7 ${r.ref_type} ${r.ref_id}` : ""}\nhash ${r.hash}\nprev_hash ${r.prev_hash}\nsig ${r.sig ?? "(unsigned)"}\nshort ${r.short}`);
|
|
366
|
+
if (verdict !== "VERIFIED")
|
|
367
|
+
process.exitCode = 1;
|
|
368
|
+
}
|
|
259
369
|
function logout() { if (existsSync(CONFIG))
|
|
260
370
|
unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
|
|
261
|
-
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 [--json] your integration + developer program numbers\n tori init [--local] wire Tori into this project (MCP + Claude Code hooks)\n tori check [paths...] [--json] run Tori's checks on changed files\n tori logout\n`); }
|
|
371
|
+
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 [--json] your integration + developer program numbers\n tori init [--local] wire Tori into this project (MCP + Claude Code hooks)\n tori check [paths...] [--json] run Tori's checks on changed files\n tori ledger [--project X|all] [--type a,b] [--since ISO] [--limit N] [--json]\n tori receipt <event_id|seq> fetch a receipt and verify it locally\n tori init --base https://sandbox.earntori.com pin this repo to a Tori environment (.tori/project.json)\n tori logout\n`); }
|
|
262
372
|
(async () => {
|
|
263
373
|
switch (cmd) {
|
|
264
374
|
case "login": return login();
|
|
265
375
|
case "status": return status();
|
|
266
376
|
case "init": return init();
|
|
267
377
|
case "check": return check();
|
|
378
|
+
case "ledger": return ledger();
|
|
379
|
+
case "receipt": return receipt();
|
|
268
380
|
case "key": return key();
|
|
269
381
|
case "stats": return stats();
|
|
270
382
|
case "logout": return logout();
|