@earntori/cli 0.2.6 → 0.2.10
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 +113 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ function out(o, human) { console.log(json ? JSON.stringify(o, null, 2) : human);
|
|
|
55
55
|
function die(msg) { console.error(json ? JSON.stringify({ error: msg }) : `\u2717 ${msg}`); process.exit(1); }
|
|
56
56
|
const usd = (c) => `$${(c / 100).toFixed(2)}`;
|
|
57
57
|
async function api(cfg, method, path, body) {
|
|
58
|
-
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.10" }, body: body ? JSON.stringify(body) : undefined });
|
|
59
59
|
const t = await r.text();
|
|
60
60
|
let j = null;
|
|
61
61
|
try {
|
|
@@ -238,6 +238,19 @@ function init() {
|
|
|
238
238
|
`\n\n${set.map((h) => h.after).join(" ")}\nTori's checks now run when the agent finishes a turn; findings are shown to the agent before it stops.` +
|
|
239
239
|
(set.some((h) => h.id === "codex") ? "" : "\n(Codex not detected \u2014 run `tori init --codex` to set it up too.)"));
|
|
240
240
|
}
|
|
241
|
+
function repoIndex() { try {
|
|
242
|
+
const out = execSync("git ls-files", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
|
|
243
|
+
return out.length ? out.slice(0, 5000) : undefined;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
} }
|
|
248
|
+
function envExample() { try {
|
|
249
|
+
return existsSync(".env.example") ? readFileSync(".env.example", "utf8").slice(0, 20000) : undefined;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
} }
|
|
241
254
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|php|java|kt|cs)$/i;
|
|
242
255
|
function projectId() {
|
|
243
256
|
try {
|
|
@@ -251,7 +264,8 @@ function projectId() {
|
|
|
251
264
|
function gitChanged() {
|
|
252
265
|
try {
|
|
253
266
|
const o = execSync("git status --porcelain --untracked-files=all", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
254
|
-
|
|
267
|
+
const SKIP = /(^|\/)(node_modules|dist|build|out|\.next|\.git|coverage|vendor|\.venv|venv|__pycache__|\.turbo|\.cache)(\/|$)/;
|
|
268
|
+
return o.split("\n").map((l) => l.slice(3).trim()).filter(Boolean).map((p) => (p.includes(" -> ") ? p.split(" -> ")[1] : p)).filter((p) => !SKIP.test(p)).slice(0, 200);
|
|
255
269
|
}
|
|
256
270
|
catch {
|
|
257
271
|
return [];
|
|
@@ -287,9 +301,27 @@ function formatRun(r, n) {
|
|
|
287
301
|
L.push(`Receipt: ${r.receipt.short}`);
|
|
288
302
|
return L.join("\n");
|
|
289
303
|
}
|
|
304
|
+
const dbg = (m) => { if (process.env.TORI_DEBUG)
|
|
305
|
+
console.error(`[tori ${new Date().toISOString().slice(11, 23)}] ${m}`); };
|
|
290
306
|
async function check() {
|
|
307
|
+
dbg("check: start");
|
|
291
308
|
if (has("reminder")) {
|
|
292
|
-
|
|
309
|
+
let extra = "";
|
|
310
|
+
try {
|
|
311
|
+
const cfg0 = loadCfg();
|
|
312
|
+
if (cfg0) {
|
|
313
|
+
const project = projectId();
|
|
314
|
+
const [st, ru] = await Promise.all([
|
|
315
|
+
api(cfg0, "GET", `/api/v1/standards?project=${encodeURIComponent(project)}`),
|
|
316
|
+
api(cfg0, "GET", `/api/v1/rules?project=${encodeURIComponent(project)}`),
|
|
317
|
+
]);
|
|
318
|
+
const domains = st.standards.filter((s) => s.tier === "domain" && s.enabled).map((s) => s.id);
|
|
319
|
+
const enforced = ru.rules.filter((r) => r.enforced).length, recorded = ru.rules.length - enforced;
|
|
320
|
+
extra = ` This project is held to Tori's coding standards${domains.length ? ` plus ${domains.join(", ")}` : ""}${ru.rules.length ? `, and ${ru.rules.length} rule${ru.rules.length === 1 ? "" : "s"} of its own (${enforced} enforced, ${recorded} recorded)` : ""}.${recorded ? " Recorded rules are not checked automatically; read them with `tori rules` and follow them." : ""}`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
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. Call ledger_query first to see open and waived findings for this project — everything here is journaled." + extra);
|
|
293
325
|
return;
|
|
294
326
|
}
|
|
295
327
|
const hook = has("hook");
|
|
@@ -300,6 +332,7 @@ async function check() {
|
|
|
300
332
|
stopActive = !!JSON.parse(raw || "{}").stop_hook_active;
|
|
301
333
|
}
|
|
302
334
|
catch { }
|
|
335
|
+
dbg(`stdin read (stop_hook_active=${stopActive})`);
|
|
303
336
|
}
|
|
304
337
|
const cfg = loadCfg();
|
|
305
338
|
if (!cfg) {
|
|
@@ -308,7 +341,10 @@ async function check() {
|
|
|
308
341
|
die("Not signed in \u2014 run: tori login");
|
|
309
342
|
}
|
|
310
343
|
const explicit = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
311
|
-
const
|
|
344
|
+
const changed = explicit.length ? explicit : gitChanged();
|
|
345
|
+
dbg(`changed files: ${changed.length}`);
|
|
346
|
+
const files = readCode(changed);
|
|
347
|
+
dbg(`code files: ${files.length} (${files.reduce((n, f) => n + f.content.length, 0)} chars)`);
|
|
312
348
|
if (files.length === 0) {
|
|
313
349
|
if (!hook)
|
|
314
350
|
out({ clean: true, files: 0 }, "Nothing to check \u2014 no changed code files.");
|
|
@@ -316,7 +352,10 @@ async function check() {
|
|
|
316
352
|
}
|
|
317
353
|
let r;
|
|
318
354
|
try {
|
|
319
|
-
const
|
|
355
|
+
const body0 = { files, project: projectId(), repo_index: repoIndex(), env_example: envExample() };
|
|
356
|
+
dbg(`request: ${JSON.stringify(body0).length} bytes, index ${body0.repo_index?.length ?? 0} paths -> ${cfg.base}`);
|
|
357
|
+
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(body0) });
|
|
358
|
+
dbg(`response: ${res.status}`);
|
|
320
359
|
const t = await res.text();
|
|
321
360
|
if (!res.ok)
|
|
322
361
|
throw new Error(`Tori API ${res.status}${t ? ": " + t.slice(0, 200) : ""}`);
|
|
@@ -328,6 +367,7 @@ async function check() {
|
|
|
328
367
|
die(e.message);
|
|
329
368
|
}
|
|
330
369
|
const text = formatRun(r, files.length);
|
|
370
|
+
const highs = r.findings.filter((f) => f.severity === "high").length, meds = r.findings.filter((f) => f.severity === "medium").length, lows = r.findings.length - highs - meds;
|
|
331
371
|
if (hook) {
|
|
332
372
|
if (r.clean)
|
|
333
373
|
return;
|
|
@@ -335,7 +375,9 @@ async function check() {
|
|
|
335
375
|
console.log(`Tori: ${r.findings.length} finding(s) still open \u2014 run \`tori check\` to see them.`);
|
|
336
376
|
return;
|
|
337
377
|
}
|
|
338
|
-
|
|
378
|
+
if (highs === 0)
|
|
379
|
+
return; // only high blocks; medium/low are recorded and shown in status and at session start
|
|
380
|
+
console.error(text + `\n\n${highs} high (blocking)${meds ? `, ${meds} medium` : ""}${lows ? `, ${lows} low` : ""} — medium and low are recorded and do not block.`);
|
|
339
381
|
process.exit(2); // exit 2 = block the stop; stderr goes to the agent
|
|
340
382
|
}
|
|
341
383
|
out(r, text);
|
|
@@ -422,7 +464,7 @@ async function connector() {
|
|
|
422
464
|
if (sub === "new") {
|
|
423
465
|
const label = flag("label") ?? "cowork";
|
|
424
466
|
const r = await api(cfg, "POST", "/api/v1/connectors", { label });
|
|
425
|
-
out(r, `\u2713 Connector "${r.label}" created (id ${r.id})\n\nAdd this URL as a custom connector (Claude Cowork / claude.ai \u2192 Settings \u2192 Connectors \u2192 Add custom connector):\n\n ${r.url}\n\nThe URL is the secret \u2014 it is shown once. Revoke with: tori connector revoke ${r.id}\nReceipt: ${r.receipt.short}`);
|
|
467
|
+
out(r, `\u2713 Connector "${r.label}" created (id ${r.id})\n\nAdd this URL as a custom connector (Claude Cowork / claude.ai \u2192 Settings \u2192 Connectors \u2192 Add custom connector):\n\n ${r.url}\n\nThe URL is the secret \u2014 it is shown once. Revoke with: tori connector revoke ${r.id}\n\nThis connector's project is connector:${r.label}. Choose its standards and add rules with:\n tori standards enable <id\u2026> --connector ${r.label}\n tori rules add "<rule>" --connector ${r.label}\nReceipt: ${r.receipt.short}`);
|
|
426
468
|
return;
|
|
427
469
|
}
|
|
428
470
|
if (sub === "revoke") {
|
|
@@ -434,11 +476,71 @@ async function connector() {
|
|
|
434
476
|
return;
|
|
435
477
|
}
|
|
436
478
|
const r = await api(cfg, "GET", "/api/v1/connectors");
|
|
437
|
-
out(r, r.connectors.length ? r.connectors.map((c) => `${c.revoked_at ? "revoked " : "active "} ${c.id} ${c.label} (${c.harness}) created ${String(c.created_at).slice(0, 10)}${c.last_used_at ? " last used " + String(c.last_used_at).slice(0, 16) : ""}`).join("\n") : "No connectors. Create one: tori connector new --label cowork");
|
|
479
|
+
out(r, r.connectors.length ? r.connectors.map((c) => `${c.revoked_at ? "revoked " : "active "} ${c.id} ${c.label} (${c.harness}) project connector:${c.label} created ${String(c.created_at).slice(0, 10)}${c.last_used_at ? " last used " + String(c.last_used_at).slice(0, 16) : ""}`).join("\n") : "No connectors. Create one: tori connector new --label cowork");
|
|
480
|
+
}
|
|
481
|
+
/** The project a standards/rules command targets: `--connector <label>` for a desktop connector's project, else this directory's. */
|
|
482
|
+
function targetProject() { const c = flag("connector"); return c ? `connector:${c}` : projectId(); }
|
|
483
|
+
async function standards() {
|
|
484
|
+
const cfg = loadCfg();
|
|
485
|
+
if (!cfg)
|
|
486
|
+
die("Not signed in \u2014 run: tori login");
|
|
487
|
+
const project = targetProject();
|
|
488
|
+
const sub = args[1];
|
|
489
|
+
if (sub === "enable" || sub === "disable") {
|
|
490
|
+
const ids = args.slice(2).filter((a) => !a.startsWith("--"));
|
|
491
|
+
if (!ids.length)
|
|
492
|
+
die(`Usage: tori standards ${sub} <standard…>`);
|
|
493
|
+
const r = await api(cfg, "POST", "/api/v1/standards", { project, enable: sub === "enable" ? ids : [], disable: sub === "disable" ? ids : [] });
|
|
494
|
+
out(r, `\u2713 ${sub === "enable" ? "Enabled" : "Disabled"} ${ids.join(", ")} for this project.\nOn for this project: coding standards (always) + ${r.enabled.length ? r.enabled.join(", ") : "no domain standards"}${r.receipts.length ? "\nReceipt: " + r.receipts.map((x) => x.short).join(", ") : ""}`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const r = await api(cfg, "GET", `/api/v1/standards?project=${encodeURIComponent(project)}`);
|
|
498
|
+
const line = (s) => ` ${s.enabled ? "on " : "off"} ${s.id.padEnd(20)} ${s.name}${s.rules ? ` (${s.rules} checks)` : " (no checks yet)"}`;
|
|
499
|
+
out(r, `${project.startsWith("connector:") ? `Project: ${project}\n\n` : ""}Coding standards \u2014 always on:\n${r.standards.filter((s) => s.tier === "coding").map(line).join("\n")}\n\nDomain standards \u2014 choose per project:\n${r.standards.filter((s) => s.tier === "domain").map(line).join("\n")}\n\nEnable: tori standards enable <id…> Disable: tori standards disable <id…>`);
|
|
500
|
+
}
|
|
501
|
+
async function rules() {
|
|
502
|
+
const cfg = loadCfg();
|
|
503
|
+
if (!cfg)
|
|
504
|
+
die("Not signed in \u2014 run: tori login");
|
|
505
|
+
const project = targetProject();
|
|
506
|
+
const sub = args[1];
|
|
507
|
+
if (sub === "add") {
|
|
508
|
+
const words = [];
|
|
509
|
+
for (let i = 2; i < args.length; i++) {
|
|
510
|
+
const a = args[i];
|
|
511
|
+
if (a.startsWith("--")) {
|
|
512
|
+
if (!a.includes("=") && i + 1 < args.length && !args[i + 1].startsWith("--"))
|
|
513
|
+
i++;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
words.push(a);
|
|
517
|
+
}
|
|
518
|
+
const text = words.join(" ").trim();
|
|
519
|
+
if (!text)
|
|
520
|
+
die('Usage: tori rules add "<rule in plain English>" [--connector <label>]');
|
|
521
|
+
const r = await api(cfg, "POST", "/api/v1/rules", { project, text });
|
|
522
|
+
if (r.existing) {
|
|
523
|
+
out(r, `Already there: "${text}" (id ${r.id}, ${r.enforced ? "enforced" : "recorded"}). Nothing added.`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
out(r, r.enforced ? `\u2713 Rule added and enforced: "${text}"\n id ${r.id} \u00b7 Tori will check it on every run. Receipt: ${r.receipt?.short}`
|
|
527
|
+
: `\u2713 Rule recorded (not enforced): "${text}"\n id ${r.id} \u00b7 Tori can't turn this wording into a check yet. The agent still sees it as a rule it is held to. Enforceable shapes: "no X [in|outside <where>]", "no X without Y", "<where> must contain X", "every <where> ships with X". Receipt: ${r.receipt?.short}`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (sub === "remove") {
|
|
531
|
+
const id = args[2];
|
|
532
|
+
if (!id || id.startsWith("--"))
|
|
533
|
+
die("Usage: tori rules remove <id>");
|
|
534
|
+
const r = await api(cfg, "DELETE", "/api/v1/rules", { project, id });
|
|
535
|
+
out(r, `\u2713 Rule ${r.id} removed. Receipt: ${r.receipt.short}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const r = await api(cfg, "GET", `/api/v1/rules?project=${encodeURIComponent(project)}`);
|
|
539
|
+
out(r, r.rules.length ? r.rules.map((x) => ` ${x.enforced ? "enforced" : "recorded"} ${x.id} ${x.text}`).join("\n") : 'No rules for this project. Add one: tori rules add "no console.log outside dev paths"');
|
|
438
540
|
}
|
|
439
541
|
function logout() { if (existsSync(CONFIG))
|
|
440
542
|
unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
|
|
441
|
-
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] [--codex|--no-codex] wire Tori into this project (Claude Code, Cursor, Codex)\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 connector new [--label X] | list | revoke <id> Tori as a remote MCP connector (Claude Cowork etc.)\n tori init --base https://sandbox.earntori.com pin this repo to a Tori environment (.tori/project.json)\n tori logout\n`); }
|
|
543
|
+
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] [--codex|--no-codex] wire Tori into this project (Claude Code, Cursor, Codex)\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 connector new [--label X] | list | revoke <id> Tori as a remote MCP connector (Claude Cowork etc.)\n tori standards [enable|disable <id…>] [--connector <label>] standards that apply to this project, or to a connector's\n tori rules [add "<rule>"|list|remove <id>] [--connector <label>] your own rules, in plain English\n tori init --base https://sandbox.earntori.com pin this repo to a Tori environment (.tori/project.json)\n tori logout\n`); }
|
|
442
544
|
(async () => {
|
|
443
545
|
switch (cmd) {
|
|
444
546
|
case "login": return login();
|
|
@@ -448,6 +550,8 @@ function help() { console.log(`tori \u2014 Tori CLI\n\n tori login [--base URL]
|
|
|
448
550
|
case "ledger": return ledger();
|
|
449
551
|
case "receipt": return receipt();
|
|
450
552
|
case "connector": return connector();
|
|
553
|
+
case "standards": return standards();
|
|
554
|
+
case "rules": return rules();
|
|
451
555
|
case "key": return key();
|
|
452
556
|
case "stats": return stats();
|
|
453
557
|
case "logout": return logout();
|