rulereceipt 0.1.63 → 0.1.64

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/README.md CHANGED
@@ -49,6 +49,17 @@ showing you exactly what it will add and asking. `protect --undo` restores the
49
49
  file byte-for-byte. It's the only place RuleReceipt writes settings, and only
50
50
  with your yes.
51
51
 
52
+ To share the result:
53
+
54
+ ```bash
55
+ npx rulereceipt card
56
+ ```
57
+
58
+ Writes a small SVG summary and prints ready-to-post links for X, LinkedIn,
59
+ Bluesky and Reddit, plus copy-text for Slack or a PR. It carries **counts
60
+ only** — no code, paths, or rule text (add rule names to the copy-text with
61
+ `--show-rules`). Nothing is posted for you and nothing is uploaded.
62
+
52
63
  ## Status
53
64
 
54
65
  Published and live on npm, actively developed.
package/dist/card.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `rulereceipt card` — a shareable image and pre-filled share links, built from
3
+ * history-mode counts. This is the "share it" step: someone runs RuleReceipt,
4
+ * sees their agent's mistakes, fixes them with `protect`, and posts the card.
5
+ *
6
+ * Privacy is the whole point of it being shareable: by default the card and the
7
+ * share text carry ONLY counts — never code, file paths, or rule text. A rule's
8
+ * name appears only when the user passes `--show-rules`, and even then only in
9
+ * the copy-text, never the image. Nothing is posted automatically and nothing
10
+ * is uploaded; the links open a compose window the user chooses to send.
11
+ */
12
+ export interface CardData {
13
+ broken: number;
14
+ followed: number;
15
+ judgment: number;
16
+ sessions: number;
17
+ days: number;
18
+ /** "Claude" when the sessions are all Claude Code, else "the agent". */
19
+ who: string;
20
+ /** Titles of the broken rules — only used with --show-rules. */
21
+ brokenTitles?: string[];
22
+ }
23
+ /** The share caption. Counts only, unless showRules adds the broken rule names. */
24
+ export declare function shareText(d: CardData, showRules?: boolean): string;
25
+ export interface ShareLinks {
26
+ x: string;
27
+ linkedin: string;
28
+ bluesky: string;
29
+ reddit: string;
30
+ }
31
+ /** Compose-window links for each network. No auth, no posting — the user sends it. */
32
+ export declare function shareLinks(text: string): ShareLinks;
33
+ /** A self-contained SVG card. Counts only — never rule text, paths or code. */
34
+ export declare function cardSvg(d: CardData): string;
35
+ /** The terminal block: where the image went, the share links, and the copy-text. */
36
+ export declare function renderCardShare(d: CardData, outPath: string, showRules?: boolean): string;
package/dist/card.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `rulereceipt card` — a shareable image and pre-filled share links, built from
3
+ * history-mode counts. This is the "share it" step: someone runs RuleReceipt,
4
+ * sees their agent's mistakes, fixes them with `protect`, and posts the card.
5
+ *
6
+ * Privacy is the whole point of it being shareable: by default the card and the
7
+ * share text carry ONLY counts — never code, file paths, or rule text. A rule's
8
+ * name appears only when the user passes `--show-rules`, and even then only in
9
+ * the copy-text, never the image. Nothing is posted automatically and nothing
10
+ * is uploaded; the links open a compose window the user chooses to send.
11
+ */
12
+ const SITE = "rulereceipt.dev";
13
+ /** The share caption. Counts only, unless showRules adds the broken rule names. */
14
+ export function shareText(d, showRules = false) {
15
+ const base = d.broken > 0
16
+ ? `${d.who} broke my written rules ${d.broken} time${d.broken === 1 ? "" : "s"} in ${d.days} days (${d.sessions} session${d.sessions === 1 ? "" : "s"}) — now it can't.`
17
+ : `RuleReceipt checked ${d.sessions} of my agent session${d.sessions === 1 ? "" : "s"} over ${d.days} days against my written rules: ${d.broken} broken.`;
18
+ const tail = `Checked with RuleReceipt — runs locally, nothing uploaded. ${SITE}`;
19
+ if (showRules && d.broken > 0 && d.brokenTitles && d.brokenTitles.length > 0) {
20
+ const list = d.brokenTitles.slice(0, 3).map((t) => `“${t.replace(/\s+/g, " ").trim().slice(0, 50)}”`).join(", ");
21
+ return `${base}\nBroke: ${list}.\n${tail}`;
22
+ }
23
+ return `${base}\n${tail}`;
24
+ }
25
+ /** Compose-window links for each network. No auth, no posting — the user sends it. */
26
+ export function shareLinks(text) {
27
+ const t = encodeURIComponent(text);
28
+ const url = encodeURIComponent(`https://${SITE}`);
29
+ const title = encodeURIComponent(text.split("\n")[0]);
30
+ return {
31
+ x: `https://twitter.com/intent/tweet?text=${t}`,
32
+ // LinkedIn's offsite share only takes a URL; the caption is added by the user.
33
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`,
34
+ bluesky: `https://bsky.app/intent/compose?text=${t}`,
35
+ reddit: `https://www.reddit.com/submit?title=${title}&url=${url}`,
36
+ };
37
+ }
38
+ function esc(s) {
39
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
40
+ }
41
+ /** A self-contained SVG card. Counts only — never rule text, paths or code. */
42
+ export function cardSvg(d) {
43
+ const headline = d.broken > 0 ? `${d.who} broke your rules ${d.broken}×` : `0 rules broken`;
44
+ const sub = `${d.sessions} session${d.sessions === 1 ? "" : "s"} · last ${d.days} days`;
45
+ const stats = `${d.followed} followed · ${d.judgment} need judgment`;
46
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="418" viewBox="0 0 800 418" role="img" aria-label="RuleReceipt summary">
47
+ <rect width="800" height="418" fill="#0b0d10"/>
48
+ <rect x="0" y="0" width="800" height="6" fill="${d.broken > 0 ? "#e5534b" : "#3fb950"}"/>
49
+ <text x="56" y="86" fill="#8b949e" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="20">RuleReceipt</text>
50
+ <text x="56" y="196" fill="#e6edf3" font-family="-apple-system, Segoe UI, Roboto, sans-serif" font-size="52" font-weight="700">${esc(headline)}</text>
51
+ <text x="56" y="248" fill="#8b949e" font-family="-apple-system, Segoe UI, Roboto, sans-serif" font-size="24">${esc(sub)}</text>
52
+ <text x="56" y="300" fill="#8b949e" font-family="-apple-system, Segoe UI, Roboto, sans-serif" font-size="22">${esc(stats)}</text>
53
+ <text x="56" y="372" fill="#6e7681" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="18">proof from real sessions · local · nothing uploaded · ${SITE}</text>
54
+ </svg>`;
55
+ }
56
+ /** The terminal block: where the image went, the share links, and the copy-text. */
57
+ export function renderCardShare(d, outPath, showRules = false) {
58
+ const text = shareText(d, showRules);
59
+ const links = shareLinks(text);
60
+ const out = [];
61
+ out.push(`Saved a shareable card to ${outPath} (counts only — no code, paths or rule text).`);
62
+ out.push("");
63
+ out.push("Share it (opens a compose window — nothing is posted for you):");
64
+ out.push(` X / Twitter ${links.x}`);
65
+ out.push(` LinkedIn ${links.linkedin}`);
66
+ out.push(` Bluesky ${links.bluesky}`);
67
+ out.push(` Reddit ${links.reddit}`);
68
+ out.push("");
69
+ out.push("Copy text (for Slack / Discord / a PR):");
70
+ out.push(text.split("\n").map((l) => ` ${l}`).join("\n"));
71
+ if (!showRules && d.broken > 0) {
72
+ out.push("");
73
+ out.push("(Rule names are left out. Add them with --show-rules if you want them in the copy-text.)");
74
+ }
75
+ return out.join("\n");
76
+ }
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { buildWrongReport, findTarget } from "./wrong.js";
18
18
  import { detectSelfEditedRuleFiles } from "./checks/selfEditedRules.js";
19
19
  import { scanHistory, renderHistory } from "./historyReport.js";
20
20
  import { planProtect, applyProtect, undoProtect } from "./protect.js";
21
+ import { cardSvg, renderCardShare } from "./card.js";
21
22
  import { createInterface } from "node:readline";
22
23
  import { loadOverrides, saveOverride, clearOverride, staleOverrides, ruleFingerprint, OVERRIDES_PATH } from "./overrides.js";
23
24
  import { runHook } from "./hook.js";
@@ -1087,6 +1088,40 @@ program
1087
1088
  console.log("The hooks call `rulereceipt` on your PATH (install once with `npm i -g rulereceipt`); they fail open if it's missing.");
1088
1089
  console.log("Undo: rulereceipt protect --undo");
1089
1090
  });
1091
+ program
1092
+ .command("card")
1093
+ .description("Make a shareable image and pre-filled share links from your last 30 days of sessions — counts only, no code, paths or rule text (add rule names to the copy-text with --show-rules). Saves an SVG locally and prints X/LinkedIn/Bluesky/Reddit compose links. Nothing is posted and nothing is uploaded.")
1094
+ .option("--out <path>", "where to write the SVG card", "rulereceipt-card.svg")
1095
+ .option("--show-rules", "include the broken rule names in the copy-text (never in the image)")
1096
+ .option("--days <n>", "how many days back to summarize", "30")
1097
+ .action(async (opts) => {
1098
+ const cwd = process.cwd();
1099
+ const rules = loadRules(cwd);
1100
+ if (rules.length === 0) {
1101
+ console.log("No rules file found for this project, so there's nothing to summarize. Add a CLAUDE.md or AGENTS.md first.");
1102
+ process.exitCode = 1;
1103
+ return;
1104
+ }
1105
+ const days = Number.parseInt(opts.days ?? "30", 10);
1106
+ const s = await scanHistory(cwd, rules, Number.isFinite(days) && days > 0 ? days : 30);
1107
+ if (s.sessionsScanned === 0) {
1108
+ console.log("No sessions found for this project in the window, so there's nothing to put on a card yet. Run your agent here, then try again.");
1109
+ process.exitCode = 1;
1110
+ return;
1111
+ }
1112
+ const data = {
1113
+ broken: s.totalBrokenCount,
1114
+ followed: s.followedRules,
1115
+ judgment: s.judgmentRules,
1116
+ sessions: s.sessionsScanned,
1117
+ days: s.days,
1118
+ who: s.tools.length === 1 && s.tools[0] === "claude-code" ? "Claude" : "the agent",
1119
+ brokenTitles: s.breaks.map((b) => b.ruleTitle),
1120
+ };
1121
+ const outPath = resolve(cwd, opts.out ?? "rulereceipt-card.svg");
1122
+ writeFileSync(outPath, cardSvg(data));
1123
+ console.log(renderCardShare(data, outPath, Boolean(opts.showRules)));
1124
+ });
1090
1125
  program
1091
1126
  .command("history")
1092
1127
  .description("Check EVERY session for this project in the last 30 days (this is what runs when you type `rulereceipt` with no arguments). Leads with the rules broken most, each with a count, the last date and one quoted line. Counts only proven breaks; judgment rules stay separate. No session to pick, no API key, nothing uploaded.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "Checks whether your AI coding agent followed your rules, with evidence. Works with Claude Code (Codex in testing); reads CLAUDE.md, AGENTS.md, Cursor, Copilot and Windsurf rules.",
5
5
  "repository": {
6
6
  "type": "git",