@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.
Files changed (2) hide show
  1. package/dist/index.js +113 -9
  2. 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.6" }, body: body ? JSON.stringify(body) : undefined });
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
- return o.split("\n").map((l) => l.slice(3).trim()).filter(Boolean).map((p) => (p.includes(" -> ") ? p.split(" -> ")[1] : p));
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
- 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.");
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 files = readCode(explicit.length ? explicit : gitChanged());
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 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() }) });
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
- console.error(text);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earntori/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.10",
4
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",