@earntori/cli 0.1.1 → 0.2.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/dist/index.js +144 -5
- package/package.json +24 -8
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// @earntori/cli — tori login | status | key --limit N [--write env] | stats | logout
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve, relative, isAbsolute, dirname } from "node:path";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
6
7
|
const CONFIG_DIR = join(homedir(), ".tori");
|
|
7
8
|
const CONFIG = join(CONFIG_DIR, "config.json");
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -21,7 +22,7 @@ function out(o, human) { console.log(json ? JSON.stringify(o, null, 2) : human);
|
|
|
21
22
|
function die(msg) { console.error(json ? JSON.stringify({ error: msg }) : `\u2717 ${msg}`); process.exit(1); }
|
|
22
23
|
const usd = (c) => `$${(c / 100).toFixed(2)}`;
|
|
23
24
|
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.
|
|
25
|
+
const r = await fetch(`${cfg.base}${path}`, { method, headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json", "User-Agent": "@earntori/cli/0.2.0" }, body: body ? JSON.stringify(body) : undefined });
|
|
25
26
|
const t = await r.text();
|
|
26
27
|
let j = null;
|
|
27
28
|
try {
|
|
@@ -106,16 +107,154 @@ async function stats() {
|
|
|
106
107
|
`Redemptions routed: ${s.redemptionsRouted}\n` +
|
|
107
108
|
`External rewards: ${s.external.issued} issued \u00B7 ${s.external.paid} paid \u00B7 ${s.external.unconfirmed} unconfirmed\n` +
|
|
108
109
|
`\nDeveloper program \u2014 credits earned: ${usd(s.program.creditsEarnedCents)}\n` +
|
|
109
|
-
` starter ${m.starter ? "\u2713" : "\u00B7"} first reward claimed ${m.firstRewardClaimed ? "\u2713" : "\u00B7"} first redemption routed ${m.firstRedemptionRouted ? "\u2713" : "\u00B7"}`
|
|
110
|
-
|
|
110
|
+
` starter ${m.starter ? "\u2713" : "\u00B7"} first reward claimed ${m.firstRewardClaimed ? "\u2713" : "\u00B7"} first redemption routed ${m.firstRedemptionRouted ? "\u2713" : "\u00B7"}`);
|
|
111
|
+
}
|
|
112
|
+
// ---- init / check (B2D core) ----------------------------------------------------------
|
|
113
|
+
// `tori init` wires Tori into the current project once: MCP server for Claude Code (.mcp.json)
|
|
114
|
+
// and Cursor (.cursor/mcp.json), plus two Claude Code hooks — Stop runs the checks on changed
|
|
115
|
+
// files before the agent finishes (findings block the stop and are shown to the agent), and
|
|
116
|
+
// SessionStart adds a one-line reminder. `--local` points at this checkout's dist/ builds
|
|
117
|
+
// instead of the published packages (for testing before publish).
|
|
118
|
+
function readJson(p) { try {
|
|
119
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
} }
|
|
124
|
+
function writeJson(p, o) { mkdirSync(dirname(resolve(p)), { recursive: true }); writeFileSync(p, JSON.stringify(o, null, 2) + "\n"); }
|
|
125
|
+
function init() {
|
|
126
|
+
const cfg = loadCfg();
|
|
127
|
+
if (!cfg)
|
|
128
|
+
die("Not signed in \u2014 run: tori login");
|
|
129
|
+
const local = has("local");
|
|
130
|
+
const cliBin = resolve(process.argv[1] ?? "");
|
|
131
|
+
const mcpBin = resolve(dirname(cliBin), "..", "..", "mcp", "dist", "index.js");
|
|
132
|
+
if (local && !existsSync(mcpBin))
|
|
133
|
+
die(`--local: MCP build not found at ${mcpBin} (run: pnpm --filter @earntori/mcp build)`);
|
|
134
|
+
const mcp = local ? { command: "node", args: [mcpBin] } : { command: "npx", args: ["-y", "@earntori/mcp"] };
|
|
135
|
+
const cli = local ? `node ${JSON.stringify(cliBin)}` : "npx -y @earntori/cli";
|
|
136
|
+
const mergeMcp = (file) => { const j = readJson(file) ?? {}; j.mcpServers = { ...(j.mcpServers ?? {}), tori: mcp }; writeJson(file, j); return file; };
|
|
137
|
+
const wrote = [mergeMcp(".mcp.json"), mergeMcp(join(".cursor", "mcp.json"))];
|
|
138
|
+
const sfile = join(".claude", "settings.json");
|
|
139
|
+
const s = readJson(sfile) ?? {};
|
|
140
|
+
s.hooks = s.hooks && typeof s.hooks === "object" ? s.hooks : {};
|
|
141
|
+
const ensure = (event, command) => {
|
|
142
|
+
const arr = Array.isArray(s.hooks[event]) ? s.hooks[event] : [];
|
|
143
|
+
const present = arr.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes("@earntori/cli check") || (local && typeof h?.command === "string" && h.command.includes(cliBin))));
|
|
144
|
+
if (!present)
|
|
145
|
+
arr.push({ hooks: [{ type: "command", command }] });
|
|
146
|
+
s.hooks[event] = arr;
|
|
147
|
+
};
|
|
148
|
+
ensure("Stop", `${cli} check --hook`);
|
|
149
|
+
ensure("SessionStart", `${cli} check --reminder`);
|
|
150
|
+
writeJson(sfile, s);
|
|
151
|
+
wrote.push(sfile);
|
|
152
|
+
out({ ok: true, wrote, local }, `\u2713 Tori installed in this project${local ? " (local builds)" : ""}\n` +
|
|
153
|
+
wrote.map((w) => ` ${w}`).join("\n") +
|
|
154
|
+
`\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.`);
|
|
155
|
+
}
|
|
156
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|php|java|kt|cs)$/i;
|
|
157
|
+
function gitChanged() {
|
|
158
|
+
try {
|
|
159
|
+
const o = execSync("git status --porcelain --untracked-files=all", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
160
|
+
return o.split("\n").map((l) => l.slice(3).trim()).filter(Boolean).map((p) => (p.includes(" -> ") ? p.split(" -> ")[1] : p));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function readCode(paths) {
|
|
167
|
+
const files = [];
|
|
168
|
+
for (const p of paths) {
|
|
169
|
+
const abs = isAbsolute(p) ? p : resolve(p);
|
|
170
|
+
const rel = relative(process.cwd(), abs);
|
|
171
|
+
if (rel.startsWith("..") || rel.includes("node_modules/") || !CODE_EXT.test(rel) || !existsSync(abs))
|
|
172
|
+
continue;
|
|
173
|
+
try {
|
|
174
|
+
const c = readFileSync(abs, "utf8");
|
|
175
|
+
if (c.length <= 200_000)
|
|
176
|
+
files.push({ path: rel, content: c });
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
}
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
function formatRun(r, n) {
|
|
183
|
+
if (r.clean)
|
|
184
|
+
return `\u2713 Tori checks clean \u2014 ${n} file(s), no open findings.`;
|
|
185
|
+
const L = [`Tori checks: ${r.findings.length} finding(s) in ${n} file(s). Fix each one, then call the checks_resolve tool with the finding_id and file path so the fix is verified.`];
|
|
186
|
+
for (const f of r.findings)
|
|
187
|
+
L.push("", `[${f.severity.toUpperCase()}] ${f.path}${f.line ? ":" + f.line : ""} (${f.rule_id})`, `finding_id: ${f.id}`, f.message, "", "How to fix:", f.fix_hint);
|
|
188
|
+
if (r.waived?.length)
|
|
189
|
+
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("; "));
|
|
190
|
+
if (r.reward?.issued)
|
|
191
|
+
L.push("", `Tori rewarded you (${r.reward.reward}) \u2014 details in your Tori ledger.`);
|
|
192
|
+
return L.join("\n");
|
|
193
|
+
}
|
|
194
|
+
async function check() {
|
|
195
|
+
if (has("reminder")) {
|
|
196
|
+
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.");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const hook = has("hook");
|
|
200
|
+
let stopActive = false;
|
|
201
|
+
if (hook) {
|
|
202
|
+
try {
|
|
203
|
+
const raw = readFileSync(0, "utf8");
|
|
204
|
+
stopActive = !!JSON.parse(raw || "{}").stop_hook_active;
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
const cfg = loadCfg();
|
|
209
|
+
if (!cfg) {
|
|
210
|
+
if (hook)
|
|
211
|
+
return;
|
|
212
|
+
die("Not signed in \u2014 run: tori login");
|
|
213
|
+
}
|
|
214
|
+
const explicit = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
215
|
+
const files = readCode(explicit.length ? explicit : gitChanged());
|
|
216
|
+
if (files.length === 0) {
|
|
217
|
+
if (!hook)
|
|
218
|
+
out({ clean: true, files: 0 }, "Nothing to check \u2014 no changed code files.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
let r;
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetch(`${cfg.base}/api/v1/checks/run`, { method: "POST", headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json", "User-Agent": "@earntori/cli/check" }, body: JSON.stringify({ files }) });
|
|
224
|
+
const t = await res.text();
|
|
225
|
+
if (!res.ok)
|
|
226
|
+
throw new Error(`Tori API ${res.status}${t ? ": " + t.slice(0, 200) : ""}`);
|
|
227
|
+
r = JSON.parse(t);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
if (hook)
|
|
231
|
+
return;
|
|
232
|
+
die(e.message);
|
|
233
|
+
}
|
|
234
|
+
const text = formatRun(r, files.length);
|
|
235
|
+
if (hook) {
|
|
236
|
+
if (r.clean)
|
|
237
|
+
return;
|
|
238
|
+
if (stopActive) {
|
|
239
|
+
console.log(`Tori: ${r.findings.length} finding(s) still open \u2014 run \`tori check\` to see them.`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.error(text);
|
|
243
|
+
process.exit(2); // exit 2 = block the stop; stderr goes to the agent
|
|
244
|
+
}
|
|
245
|
+
out(r, text);
|
|
246
|
+
if (!r.clean)
|
|
247
|
+
process.exitCode = 1;
|
|
111
248
|
}
|
|
112
249
|
function logout() { if (existsSync(CONFIG))
|
|
113
250
|
unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
|
|
114
|
-
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 logout\n`); }
|
|
251
|
+
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`); }
|
|
115
252
|
(async () => {
|
|
116
253
|
switch (cmd) {
|
|
117
254
|
case "login": return login();
|
|
118
255
|
case "status": return status();
|
|
256
|
+
case "init": return init();
|
|
257
|
+
case "check": return check();
|
|
119
258
|
case "key": return key();
|
|
120
259
|
case "stats": return stats();
|
|
121
260
|
case "logout": return logout();
|
package/package.json
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@earntori/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Tori CLI
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Tori CLI \u2014 sign in with GitHub, check your credits, mint a capped AI key into your tools.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"tori": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
19
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"test": "echo no tests"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.6.0",
|
|
24
|
+
"@types/node": "^20.0.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
13
29
|
}
|