@earntori/cli 0.2.9 → 0.2.11

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 +74 -19
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readdirSync } from "node:fs";
2
3
  // @earntori/cli — tori login | status | key --limit N [--write env] | stats | logout
3
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync, unlinkSync } from "node:fs";
4
5
  import { homedir } from "node:os";
@@ -55,7 +56,7 @@ function out(o, human) { console.log(json ? JSON.stringify(o, null, 2) : human);
55
56
  function die(msg) { console.error(json ? JSON.stringify({ error: msg }) : `\u2717 ${msg}`); process.exit(1); }
56
57
  const usd = (c) => `$${(c / 100).toFixed(2)}`;
57
58
  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.9" }, body: body ? JSON.stringify(body) : undefined });
59
+ 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
60
  const t = await r.text();
60
61
  let j = null;
61
62
  try {
@@ -157,18 +158,37 @@ catch {
157
158
  return null;
158
159
  } }
159
160
  function writeJson(p, o) { mkdirSync(dirname(resolve(p)), { recursive: true }); writeFileSync(p, JSON.stringify(o, null, 2) + "\n"); }
161
+ // Harness registry: one entry per coding harness. `detect` decides whether `tori init` sets it up
162
+ // (Claude Code and Cursor: always; Codex: when it is installed or present in the repo, or --codex);
163
+ // `install` writes that harness's files and returns their paths. Add a harness = add a row.
164
+ // Pinned package versions written into agent config (B1b). CLI version = this package's own;
165
+ // MCP version is a constant: bump it in the same commit as a @earntori/mcp release.
166
+ const TORI_MCP_VERSION = "0.1.7";
167
+ function cliVersion(cliBin) {
168
+ try {
169
+ return JSON.parse(readFileSync(resolve(dirname(cliBin), "..", "package.json"), "utf8")).version ?? "latest";
170
+ }
171
+ catch {
172
+ return "latest";
173
+ }
174
+ }
160
175
  const withHarness = (mcp, id) => ({ ...mcp, env: { TORI_HARNESS: id } });
161
176
  function mergeMcpJson(file, mcp) { const j = readJson(file) ?? {}; j.mcpServers = { ...(j.mcpServers ?? {}), tori: mcp }; writeJson(file, j); return file; }
162
- function hookPresent(h, c) { return typeof h?.command === "string" && (h.command.includes("@earntori/cli check") || (c.local && h.command.includes(c.cliBin))); }
177
+ function hookPresent(h, c) { return typeof h?.command === "string" && (/@earntori\/cli(@[^\s]+)? check/.test(h.command) || (c.local && h.command.includes(c.cliBin))); }
178
+ // Re-running init replaces an existing Tori hook group (so a new pinned version lands) and never adds a second one.
163
179
  function ensureHook(hooks, event, entry, c) {
164
180
  const arr = Array.isArray(hooks[event]) ? hooks[event] : [];
165
- if (!arr.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => hookPresent(h, c))))
181
+ const i = arr.findIndex((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => hookPresent(h, c)));
182
+ if (i >= 0)
183
+ arr[i] = entry;
184
+ else
166
185
  arr.push(entry);
167
186
  hooks[event] = arr;
168
187
  }
169
188
  const HARNESSES = [
170
189
  {
171
- id: "claude-code", label: "Claude Code", detect: () => true,
190
+ id: "claude-code", label: "Claude Code",
191
+ detect: () => !has("no-claude") && (has("claude") || existsSync(".claude") || existsSync(".mcp.json") || existsSync(join(homedir(), ".claude")) || existsSync(join(homedir(), ".claude.json"))),
172
192
  install: (c) => {
173
193
  const wrote = [mergeMcpJson(".mcp.json", withHarness(c.mcp, "claude-code"))];
174
194
  const sfile = join(".claude", "settings.json");
@@ -183,7 +203,8 @@ const HARNESSES = [
183
203
  after: "Claude Code: restart the session (hooks and .mcp.json load at start).",
184
204
  },
185
205
  {
186
- id: "cursor", label: "Cursor", detect: () => true,
206
+ id: "cursor", label: "Cursor",
207
+ detect: () => !has("no-cursor") && (has("cursor") || existsSync(".cursor") || existsSync(join(homedir(), ".cursor"))),
187
208
  install: (c) => [mergeMcpJson(join(".cursor", "mcp.json"), withHarness(c.mcp, "cursor"))],
188
209
  after: "Cursor: reload the window.",
189
210
  },
@@ -200,6 +221,12 @@ const HARNESSES = [
200
221
  mkdirSync(".codex", { recursive: true });
201
222
  writeFileSync(tfile, (toml.trimEnd() + "\n" + block).replace(/^\n/, ""));
202
223
  }
224
+ else {
225
+ // Existing block: rewrite its command/args so the pinned version is current.
226
+ const updated = toml.replace(/(\[mcp_servers\.tori\][^\[]*?)command = [^\n]*\nargs = [^\n]*/, `$1command = ${JSON.stringify(c.mcp.command)}\nargs = ${JSON.stringify(c.mcp.args)}`);
227
+ if (updated !== toml)
228
+ writeFileSync(tfile, updated);
229
+ }
203
230
  wrote.push(tfile);
204
231
  // .codex/hooks.json — same two hooks; Stop uses exit 2 + stderr to continue the turn, SessionStart prints to stdout.
205
232
  const hfile = join(".codex", "hooks.json");
@@ -226,8 +253,11 @@ function init() {
226
253
  const mcpBin = resolve(dirname(cliBin), "..", "..", "mcp", "dist", "index.js");
227
254
  if (local && !existsSync(mcpBin))
228
255
  die(`--local: MCP build not found at ${mcpBin} (run: pnpm --filter @earntori/mcp build)`);
229
- const ctx = { mcp: local ? { command: "node", args: [mcpBin] } : { command: "npx", args: ["-y", "@earntori/mcp"] }, cli: local ? `node ${JSON.stringify(cliBin)}` : "npx -y @earntori/cli", local, cliBin };
256
+ const ver = cliVersion(cliBin);
257
+ const ctx = { mcp: local ? { command: "node", args: [mcpBin] } : { command: "npx", args: ["-y", `@earntori/mcp@${TORI_MCP_VERSION}`] }, cli: local ? `node ${JSON.stringify(cliBin)}` : `npx -y @earntori/cli@${ver}`, local, cliBin };
230
258
  const set = HARNESSES.filter((h) => h.detect());
259
+ if (!set.length)
260
+ die("No coding agent detected here. Run `tori init --claude`, `--cursor` or `--codex` to set one up anyway.");
231
261
  const wrote = [];
232
262
  for (const h of set)
233
263
  wrote.push(...h.install(ctx));
@@ -236,7 +266,8 @@ function init() {
236
266
  out({ ok: true, harnesses: set.map((h) => h.id), wrote, local }, `\u2713 Tori installed in this project for ${set.map((h) => h.label).join(", ")}${local ? " (local builds)" : ""}${pin ? ` \u2014 pinned to ${pin}` : ""}\n` +
237
267
  wrote.map((w) => ` ${w}`).join("\n") +
238
268
  `\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
- (set.some((h) => h.id === "codex") ? "" : "\n(Codex not detected \u2014 run `tori init --codex` to set it up too.)"));
269
+ (local ? "" : `\nPinned to @earntori/cli@${ver} and @earntori/mcp@${TORI_MCP_VERSION}; re-run tori init after upgrading.`) +
270
+ (set.length === HARNESSES.length ? "" : "\n(Not detected: " + HARNESSES.filter((h) => !set.includes(h)).map((h) => `${h.label} \u2014 tori init --${h.id === "claude-code" ? "claude" : h.id}`).join("; ") + ".)"));
240
271
  }
241
272
  function repoIndex() { try {
242
273
  const out = execSync("git ls-files", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
@@ -464,7 +495,7 @@ async function connector() {
464
495
  if (sub === "new") {
465
496
  const label = flag("label") ?? "cowork";
466
497
  const r = await api(cfg, "POST", "/api/v1/connectors", { label });
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}\nReceipt: ${r.receipt.short}`);
498
+ 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}`);
468
499
  return;
469
500
  }
470
501
  if (sub === "revoke") {
@@ -476,13 +507,15 @@ async function connector() {
476
507
  return;
477
508
  }
478
509
  const r = await api(cfg, "GET", "/api/v1/connectors");
479
- 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");
510
+ 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
511
  }
512
+ /** The project a standards/rules command targets: `--connector <label>` for a desktop connector's project, else this directory's. */
513
+ function targetProject() { const c = flag("connector"); return c ? `connector:${c}` : projectId(); }
481
514
  async function standards() {
482
515
  const cfg = loadCfg();
483
516
  if (!cfg)
484
517
  die("Not signed in \u2014 run: tori login");
485
- const project = projectId();
518
+ const project = targetProject();
486
519
  const sub = args[1];
487
520
  if (sub === "enable" || sub === "disable") {
488
521
  const ids = args.slice(2).filter((a) => !a.startsWith("--"));
@@ -494,21 +527,35 @@ async function standards() {
494
527
  }
495
528
  const r = await api(cfg, "GET", `/api/v1/standards?project=${encodeURIComponent(project)}`);
496
529
  const line = (s) => ` ${s.enabled ? "on " : "off"} ${s.id.padEnd(20)} ${s.name}${s.rules ? ` (${s.rules} checks)` : " (no checks yet)"}`;
497
- out(r, `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…>`);
530
+ 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…>`);
498
531
  }
499
532
  async function rules() {
500
533
  const cfg = loadCfg();
501
534
  if (!cfg)
502
535
  die("Not signed in \u2014 run: tori login");
503
- const project = projectId();
536
+ const project = targetProject();
504
537
  const sub = args[1];
505
538
  if (sub === "add") {
506
- const text = args.slice(2).filter((a) => !a.startsWith("--")).join(" ").trim();
539
+ const words = [];
540
+ for (let i = 2; i < args.length; i++) {
541
+ const a = args[i];
542
+ if (a.startsWith("--")) {
543
+ if (!a.includes("=") && i + 1 < args.length && !args[i + 1].startsWith("--"))
544
+ i++;
545
+ continue;
546
+ }
547
+ words.push(a);
548
+ }
549
+ const text = words.join(" ").trim();
507
550
  if (!text)
508
- die('Usage: tori rules add "<rule in plain English>"');
551
+ die('Usage: tori rules add "<rule in plain English>" [--connector <label>]');
509
552
  const r = await api(cfg, "POST", "/api/v1/rules", { project, text });
510
- 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}`
511
- : `\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>]", "<where> must contain X", "every <where> ships with X". Receipt: ${r.receipt.short}`);
553
+ if (r.existing) {
554
+ out(r, `Already there: "${text}" (id ${r.id}, ${r.enforced ? "enforced" : "recorded"}). Nothing added.`);
555
+ return;
556
+ }
557
+ 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}`
558
+ : `\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}`);
512
559
  return;
513
560
  }
514
561
  if (sub === "remove") {
@@ -522,9 +569,17 @@ async function rules() {
522
569
  const r = await api(cfg, "GET", `/api/v1/rules?project=${encodeURIComponent(project)}`);
523
570
  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"');
524
571
  }
525
- function logout() { if (existsSync(CONFIG))
526
- unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
527
- 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…>] standards that apply to this project (coding standards are always on)\n tori rules [add "<rule>"|list|remove <id>] your own rules for this project, 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`); }
572
+ function logout() {
573
+ if (existsSync(CONFIG))
574
+ unlinkSync(CONFIG);
575
+ // Pinned projects keep a per-base session; signing out must remove those too (B6).
576
+ if (existsSync(SESSIONS_DIR))
577
+ for (const f of readdirSync(SESSIONS_DIR))
578
+ if (f.endsWith(".json"))
579
+ unlinkSync(join(SESSIONS_DIR, f));
580
+ out({ ok: true }, "\u2713 Signed out (all sessions removed)");
581
+ }
582
+ 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] [--claude|--no-claude] [--cursor|--no-cursor] [--codex|--no-codex] wire Tori into this project (agents detected unless forced)\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`); }
528
583
  (async () => {
529
584
  switch (cmd) {
530
585
  case "login": return login();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earntori/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",
@@ -29,7 +29,7 @@
29
29
  "homepage": "https://www.earntori.com/docs/mcp",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/GAishZOEish/Tori-MCP.git"
32
+ "url": "git+https://github.com/usetori/tori.git"
33
33
  },
34
34
  "bugs": {
35
35
  "url": "https://github.com/GAishZOEish/Tori-MCP/issues"