@earntori/cli 0.2.4 → 0.2.6
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 +96 -25
- 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.6" }, body: body ? JSON.stringify(body) : undefined });
|
|
59
59
|
const t = await r.text();
|
|
60
60
|
let j = null;
|
|
61
61
|
try {
|
|
@@ -119,8 +119,9 @@ async function key() {
|
|
|
119
119
|
let note = "";
|
|
120
120
|
if (write === "env") {
|
|
121
121
|
const file = flag("file") ?? ".env";
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const pinnedBase = projectBase() ?? (cfg.base !== "https://www.earntori.com" ? cfg.base : null);
|
|
123
|
+
appendFileSync(file, `\n# Tori AI key (${r.label}, cap ${usd(r.limitCents)})\nOPENAI_API_KEY=${r.key}\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\n${pinnedBase ? `TORI_BASE_URL=${pinnedBase}\n` : ""}`);
|
|
124
|
+
note = `\n\u2713 Wrote OPENAI_API_KEY and OPENAI_BASE_URL${pinnedBase ? " and TORI_BASE_URL" : ""} to ${file}`;
|
|
124
125
|
}
|
|
125
126
|
else if (write)
|
|
126
127
|
die(`--write ${write} is not supported yet; use --write env`);
|
|
@@ -156,6 +157,63 @@ catch {
|
|
|
156
157
|
return null;
|
|
157
158
|
} }
|
|
158
159
|
function writeJson(p, o) { mkdirSync(dirname(resolve(p)), { recursive: true }); writeFileSync(p, JSON.stringify(o, null, 2) + "\n"); }
|
|
160
|
+
const withHarness = (mcp, id) => ({ ...mcp, env: { TORI_HARNESS: id } });
|
|
161
|
+
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))); }
|
|
163
|
+
function ensureHook(hooks, event, entry, c) {
|
|
164
|
+
const arr = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
165
|
+
if (!arr.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => hookPresent(h, c))))
|
|
166
|
+
arr.push(entry);
|
|
167
|
+
hooks[event] = arr;
|
|
168
|
+
}
|
|
169
|
+
const HARNESSES = [
|
|
170
|
+
{
|
|
171
|
+
id: "claude-code", label: "Claude Code", detect: () => true,
|
|
172
|
+
install: (c) => {
|
|
173
|
+
const wrote = [mergeMcpJson(".mcp.json", withHarness(c.mcp, "claude-code"))];
|
|
174
|
+
const sfile = join(".claude", "settings.json");
|
|
175
|
+
const s = readJson(sfile) ?? {};
|
|
176
|
+
s.hooks = s.hooks && typeof s.hooks === "object" ? s.hooks : {};
|
|
177
|
+
ensureHook(s.hooks, "Stop", { hooks: [{ type: "command", command: `${c.cli} check --hook` }] }, c);
|
|
178
|
+
ensureHook(s.hooks, "SessionStart", { hooks: [{ type: "command", command: `${c.cli} check --reminder` }] }, c);
|
|
179
|
+
writeJson(sfile, s);
|
|
180
|
+
wrote.push(sfile);
|
|
181
|
+
return wrote;
|
|
182
|
+
},
|
|
183
|
+
after: "Claude Code: restart the session (hooks and .mcp.json load at start).",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "cursor", label: "Cursor", detect: () => true,
|
|
187
|
+
install: (c) => [mergeMcpJson(join(".cursor", "mcp.json"), withHarness(c.mcp, "cursor"))],
|
|
188
|
+
after: "Cursor: reload the window.",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "codex", label: "Codex",
|
|
192
|
+
detect: () => !has("no-codex") && (has("codex") || existsSync(".codex") || existsSync(join(homedir(), ".codex"))),
|
|
193
|
+
install: (c) => {
|
|
194
|
+
const wrote = [];
|
|
195
|
+
// .codex/config.toml — append the [mcp_servers.tori] table if it is not already there.
|
|
196
|
+
const tfile = join(".codex", "config.toml");
|
|
197
|
+
const toml = existsSync(tfile) ? readFileSync(tfile, "utf8") : "";
|
|
198
|
+
if (!/\[mcp_servers\.tori\]/.test(toml)) {
|
|
199
|
+
const block = `\n[mcp_servers.tori]\ncommand = ${JSON.stringify(c.mcp.command)}\nargs = ${JSON.stringify(c.mcp.args)}\n\n[mcp_servers.tori.env]\nTORI_HARNESS = "codex"\n`;
|
|
200
|
+
mkdirSync(".codex", { recursive: true });
|
|
201
|
+
writeFileSync(tfile, (toml.trimEnd() + "\n" + block).replace(/^\n/, ""));
|
|
202
|
+
}
|
|
203
|
+
wrote.push(tfile);
|
|
204
|
+
// .codex/hooks.json — same two hooks; Stop uses exit 2 + stderr to continue the turn, SessionStart prints to stdout.
|
|
205
|
+
const hfile = join(".codex", "hooks.json");
|
|
206
|
+
const h = readJson(hfile) ?? {};
|
|
207
|
+
h.hooks = h.hooks && typeof h.hooks === "object" ? h.hooks : {};
|
|
208
|
+
ensureHook(h.hooks, "SessionStart", { matcher: "startup|resume", hooks: [{ type: "command", command: `${c.cli} check --reminder`, timeout: 30 }] }, c);
|
|
209
|
+
ensureHook(h.hooks, "Stop", { hooks: [{ type: "command", command: `${c.cli} check --hook`, timeout: 120 }] }, c);
|
|
210
|
+
writeJson(hfile, h);
|
|
211
|
+
wrote.push(hfile);
|
|
212
|
+
return wrote;
|
|
213
|
+
},
|
|
214
|
+
after: "Codex: trust the project, then /mcp to confirm tori and /hooks once to trust the two hooks.",
|
|
215
|
+
},
|
|
216
|
+
];
|
|
159
217
|
function init() {
|
|
160
218
|
const pin = flag("base");
|
|
161
219
|
if (pin)
|
|
@@ -168,29 +226,17 @@ function init() {
|
|
|
168
226
|
const mcpBin = resolve(dirname(cliBin), "..", "..", "mcp", "dist", "index.js");
|
|
169
227
|
if (local && !existsSync(mcpBin))
|
|
170
228
|
die(`--local: MCP build not found at ${mcpBin} (run: pnpm --filter @earntori/mcp build)`);
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const s = readJson(sfile) ?? {};
|
|
177
|
-
s.hooks = s.hooks && typeof s.hooks === "object" ? s.hooks : {};
|
|
178
|
-
const ensure = (event, command) => {
|
|
179
|
-
const arr = Array.isArray(s.hooks[event]) ? s.hooks[event] : [];
|
|
180
|
-
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))));
|
|
181
|
-
if (!present)
|
|
182
|
-
arr.push({ hooks: [{ type: "command", command }] });
|
|
183
|
-
s.hooks[event] = arr;
|
|
184
|
-
};
|
|
185
|
-
ensure("Stop", `${cli} check --hook`);
|
|
186
|
-
ensure("SessionStart", `${cli} check --reminder`);
|
|
187
|
-
writeJson(sfile, s);
|
|
188
|
-
wrote.push(sfile);
|
|
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 };
|
|
230
|
+
const set = HARNESSES.filter((h) => h.detect());
|
|
231
|
+
const wrote = [];
|
|
232
|
+
for (const h of set)
|
|
233
|
+
wrote.push(...h.install(ctx));
|
|
189
234
|
if (pin)
|
|
190
235
|
wrote.push(join(".tori", "project.json"));
|
|
191
|
-
out({ ok: true, wrote, local }, `\u2713 Tori installed in this project${local ? " (local builds)" : ""}${pin ? ` \u2014 pinned to ${pin}` : ""}\n` +
|
|
236
|
+
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` +
|
|
192
237
|
wrote.map((w) => ` ${w}`).join("\n") +
|
|
193
|
-
`\n\
|
|
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
|
+
(set.some((h) => h.id === "codex") ? "" : "\n(Codex not detected \u2014 run `tori init --codex` to set it up too.)"));
|
|
194
240
|
}
|
|
195
241
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|php|java|kt|cs)$/i;
|
|
196
242
|
function projectId() {
|
|
@@ -243,7 +289,7 @@ function formatRun(r, n) {
|
|
|
243
289
|
}
|
|
244
290
|
async function check() {
|
|
245
291
|
if (has("reminder")) {
|
|
246
|
-
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.
|
|
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.");
|
|
247
293
|
return;
|
|
248
294
|
}
|
|
249
295
|
const hook = has("hook");
|
|
@@ -318,6 +364,8 @@ async function ledger() {
|
|
|
318
364
|
const line = (e) => {
|
|
319
365
|
const p = e.payload ?? {};
|
|
320
366
|
const bits = [];
|
|
367
|
+
if (p.summary)
|
|
368
|
+
bits.push(String(p.summary).slice(0, 120));
|
|
321
369
|
if (p.rule_id)
|
|
322
370
|
bits.push(p.rule_id);
|
|
323
371
|
if (p.path)
|
|
@@ -366,9 +414,31 @@ async function receipt() {
|
|
|
366
414
|
if (verdict !== "VERIFIED")
|
|
367
415
|
process.exitCode = 1;
|
|
368
416
|
}
|
|
417
|
+
async function connector() {
|
|
418
|
+
const cfg = loadCfg();
|
|
419
|
+
if (!cfg)
|
|
420
|
+
die("Not signed in \u2014 run: tori login");
|
|
421
|
+
const sub = args[1];
|
|
422
|
+
if (sub === "new") {
|
|
423
|
+
const label = flag("label") ?? "cowork";
|
|
424
|
+
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}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (sub === "revoke") {
|
|
429
|
+
const id = args[2];
|
|
430
|
+
if (!id || id.startsWith("--"))
|
|
431
|
+
die("Usage: tori connector revoke <id>");
|
|
432
|
+
const r = await api(cfg, "POST", `/api/v1/connectors/${encodeURIComponent(id)}/revoke`, {});
|
|
433
|
+
out(r, `\u2713 Connector ${r.id} revoked. Receipt: ${r.receipt.short}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
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");
|
|
438
|
+
}
|
|
369
439
|
function logout() { if (existsSync(CONFIG))
|
|
370
440
|
unlinkSync(CONFIG); out({ ok: true }, "\u2713 Signed out"); }
|
|
371
|
-
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]
|
|
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`); }
|
|
372
442
|
(async () => {
|
|
373
443
|
switch (cmd) {
|
|
374
444
|
case "login": return login();
|
|
@@ -377,6 +447,7 @@ function help() { console.log(`tori \u2014 Tori CLI\n\n tori login [--base URL]
|
|
|
377
447
|
case "check": return check();
|
|
378
448
|
case "ledger": return ledger();
|
|
379
449
|
case "receipt": return receipt();
|
|
450
|
+
case "connector": return connector();
|
|
380
451
|
case "key": return key();
|
|
381
452
|
case "stats": return stats();
|
|
382
453
|
case "logout": return logout();
|