@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.
- package/dist/index.js +74 -19
- 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.
|
|
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" && (
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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
|
-
(
|
|
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 =
|
|
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 =
|
|
536
|
+
const project = targetProject();
|
|
504
537
|
const sub = args[1];
|
|
505
538
|
if (sub === "add") {
|
|
506
|
-
const
|
|
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
|
-
|
|
511
|
-
|
|
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() {
|
|
526
|
-
|
|
527
|
-
|
|
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.
|
|
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/
|
|
32
|
+
"url": "git+https://github.com/usetori/tori.git"
|
|
33
33
|
},
|
|
34
34
|
"bugs": {
|
|
35
35
|
"url": "https://github.com/GAishZOEish/Tori-MCP/issues"
|