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