toldya 0.3.0 → 0.3.1

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/bin/toldya.mjs +184 -181
  2. package/package.json +1 -1
package/bin/toldya.mjs CHANGED
@@ -1,181 +1,184 @@
1
- #!/usr/bin/env node
2
- // toldya — stop repeating yourself to your AI.
3
- //
4
- // Reads what you typed to Claude Code (from its own history on this machine),
5
- // finds the corrections you keep repeating, offers each one as a line for the
6
- // rule file your agent reads, and next time counts whether you still say it.
7
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
8
- import { spawn } from 'node:child_process';
9
- import { join, resolve } from 'node:path';
10
- import { homedir, tmpdir } from 'node:os';
11
- import { createInterface } from 'node:readline/promises';
12
- import {
13
- CLAUDE_ROOT, claudeProjects, projectFor, readClaudeSession,
14
- corrections, group, repeats, toRule, alreadyWritten, beforeAfter, isRetry,
15
- } from '../lib/core.mjs';
16
- import { cardHtml } from '../lib/card.mjs';
17
-
18
- const VERSION = '0.3.0';
19
- const argv = process.argv.slice(2);
20
- const has = (f) => argv.includes(f);
21
- const opt = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
22
-
23
- if (has('--help') || has('-h')) {
24
- console.log(`toldya ${VERSION} — stop repeating yourself to your AI
25
-
26
- toldya this project: what you keep telling Claude Code here
27
- toldya --all every project; rules go to your global ~/.claude/CLAUDE.md
28
- --min N only repeats said at least N times (default 3)
29
- --to FILE write rules to FILE instead (e.g. AGENTS.md)
30
- --add 1,3 add repeats by their number, without asking
31
- --dry show the report, change nothing
32
- --card make an image of your top repeats, to share
33
- --json machine-readable report, change nothing
34
-
35
- Reads Claude Code's own history on this machine. Sends nothing anywhere.`);
36
- process.exit(0);
37
- }
38
- if (has('--version') || has('-v')) { console.log(VERSION); process.exit(0); }
39
- if (argv.some((a) => /^[\w.-]+\/[\w.-]+$/.test(a))) {
40
- console.log('Team mode (owner/repo) is coming next. For now: run `npx toldya` in your project.');
41
- process.exit(0);
42
- }
43
-
44
- const all = has('--all');
45
- const min = Math.max(2, parseInt(opt('--min', '3'), 10) || 3);
46
- const cwd = process.cwd();
47
- const projects = claudeProjects();
48
- const chosen = all ? projects : [projectFor(cwd, projects)].filter(Boolean);
49
-
50
- if (!chosen.length) {
51
- console.log(projects.length
52
- ? `No Claude Code history for this folder (${cwd}).\nRun it inside a project you've used Claude Code in, or try: npx toldya --all`
53
- : `No Claude Code history found at ${CLAUDE_ROOT}.`);
54
- process.exit(0);
55
- }
56
-
57
- // Everything you typed, stamped with the session it came from.
58
- const messages = [];
59
- let sessions = 0;
60
- for (const p of chosen) {
61
- const dir = join(CLAUDE_ROOT, p);
62
- for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsonl'))) {
63
- const ms = readClaudeSession(join(dir, f));
64
- if (ms.length) sessions++;
65
- for (const m of ms) messages.push({ ...m, session: `${p}/${f}` });
66
- }
67
- }
68
- const said = corrections(messages);
69
- const retries = said.filter((i) => isRetry(i.s)).length;
70
- const items = said.filter((i) => !isRetry(i.s));
71
- const found = repeats(group(items), min);
72
-
73
- // Where rules go, and what's already written there.
74
- const target = resolve(opt('--to', all ? join(homedir(), '.claude', 'CLAUDE.md') : join(cwd, 'CLAUDE.md')));
75
- const ruleFiles = [...new Set(all ? [target] : [target, join(cwd, 'CLAUDE.md'), join(cwd, 'AGENTS.md')])];
76
- const written = ruleFiles.filter(existsSync).map((f) => readFileSync(f, 'utf8')).join('\n');
77
-
78
- // Rules toldya added before, so we can count whether they worked.
79
- const stateDir = join(homedir(), '.toldya');
80
- const stateFile = join(stateDir, 'state.json');
81
- // Rules are remembered per rule file: that's where they live.
82
- const key = target;
83
- let state = {};
84
- try { state = JSON.parse(readFileSync(stateFile, 'utf8')); } catch {}
85
- const ours = state[key]?.rules || [];
86
-
87
- const dates = messages.map((m) => m.ts).filter(Boolean).sort();
88
- const day = (t) => (t ? new Date(t).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '?');
89
-
90
- if (has('--json')) {
91
- console.log(JSON.stringify({
92
- sessions, messages: messages.length, corrections: items.length, retries,
93
- from: dates[0] || null, to: dates.at(-1) || null,
94
- repeats: found.map((r) => ({ phrase: r.phrase, count: r.count, sessions: r.sessions,
95
- alreadyWritten: alreadyWritten(r.phrase, written) })),
96
- rules: ours.map((r) => ({ ...r, ...beforeAfter(r, items) })),
97
- }, null, 2));
98
- process.exit(0);
99
- }
100
-
101
- console.log(`\ntoldya · ${sessions} sessions (${day(dates[0])} – ${day(dates.at(-1))}) · ${messages.length} of your messages · ${items.length} corrections\n`);
102
-
103
- if (ours.length) {
104
- console.log('Rules toldya added earlier:');
105
- for (const r of ours) {
106
- const { before, after } = beforeAfter(r, items);
107
- console.log(` "${r.text}" said ${before}× before · ${after}× since (added ${day(r.addedAt)})`);
108
- }
109
- console.log('');
110
- }
111
-
112
- const fresh = found.filter((r) => !ours.some((o) => o.text === toRule(r.phrase)));
113
- if (!fresh.length) {
114
- console.log(found.length ? 'Nothing new you keep repeating. Nice.' : `No correction said ${min}+ times across sessions yet.`);
115
- process.exit(0);
116
- }
117
-
118
- if (has('--card')) {
119
- if (!found.length) { console.log(`No correction said ${min}+ times yet, so no card.`); process.exit(0); }
120
- const file = join(tmpdir(), 'toldya-card.html');
121
- writeFileSync(file, cardHtml({ repeats: found, sessions, from: day(dates[0]), to: day(dates.at(-1)) }));
122
- const open = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', file]]
123
- : [process.platform === 'darwin' ? 'open' : 'xdg-open', [file]];
124
- try { spawn(open[0], open[1], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref(); } catch {}
125
- console.log(`Your card: ${file}
126
- It opens in your browser: save it as a PNG, then post it. Nothing leaves your machine unless you click share.`);
127
- process.exit(0);
128
- }
129
-
130
- console.log('You keep telling your AI:');
131
- const shown = fresh.slice(0, 10);
132
- shown.forEach((r, n) => {
133
- const note = alreadyWritten(r.phrase, written) ? ' ← already in your rules, still repeated' : '';
134
- console.log(` ${String(n + 1).padStart(2)}. ${String(r.count).padStart(3)}× ${r.phrase} (${r.sessions} sessions)${note}`);
135
- });
136
- if (retries >= min) console.log(`\nAnd ${retries} times you told it to try or check again: its first go missed.`);
137
- const spanDays = dates.length ? (new Date(dates.at(-1)) - new Date(dates[0])) / 864e5 : 0;
138
- if (spanDays < 35) console.log('\nClaude Code keeps about 30 days of history by default, so older repeats are not counted.');
139
-
140
- const picks = opt('--add', null);
141
- if (has('--dry') || (!picks && !process.stdin.isTTY)) {
142
- if (!has('--dry')) console.log('\nRun in a terminal to add these as rules, or pick them with --add 1,3.');
143
- process.exit(0);
144
- }
145
-
146
- // Ask before touching any file (or take the numbers given). Each rule is your own words.
147
- const add = [];
148
- if (picks) {
149
- for (const n of String(picks).split(',').map((x) => parseInt(x, 10))) {
150
- const r = shown[n - 1];
151
- if (r && !alreadyWritten(r.phrase, written)) add.push(toRule(r.phrase));
152
- }
153
- } else {
154
- const rl = createInterface({ input: process.stdin, output: process.stdout });
155
- console.log(`\nAdd them to ${target}? For each: y = add, n = skip, e = edit the wording.\n`);
156
- for (const r of shown) {
157
- if (alreadyWritten(r.phrase, written)) continue;
158
- let text = toRule(r.phrase);
159
- const a = (await rl.question(` ${text} [y/n/e] `)).trim().toLowerCase();
160
- if (a === 'e') text = toRule((await rl.question(' new wording: ')).trim() || r.phrase);
161
- if (a === 'y' || a === 'e') add.push(text);
162
- }
163
- rl.close();
164
- }
165
-
166
- if (!add.length) { console.log('\nNothing added.'); process.exit(0); }
167
-
168
- const heading = '## Things I kept repeating';
169
- let doc = existsSync(target) ? readFileSync(target, 'utf8') : '';
170
- const block = add.map((t) => `- ${t}`).join('\n');
171
- if (doc.includes(heading)) doc = doc.replace(heading, `${heading}\n${block}`);
172
- else doc = `${doc.replace(/\s*$/, '')}${doc ? '\n\n' : ''}${heading}\n${block}\n`;
173
- mkdirSync(join(target, '..'), { recursive: true });
174
- writeFileSync(target, doc);
175
-
176
- const now = new Date().toISOString();
177
- state[key] = { rules: [...ours, ...add.map((text) => ({ text, addedAt: now }))] };
178
- mkdirSync(stateDir, { recursive: true });
179
- writeFileSync(stateFile, JSON.stringify(state, null, 2));
180
- const shortTarget = target.startsWith(cwd) ? target.slice(cwd.length + 1) : target.replace(homedir(), '~');
181
- console.log(`\nAdded ${add.length} rule${add.length > 1 ? 's' : ''} to ${shortTarget}. Run toldya again in a week to see if they stuck.`);
1
+ #!/usr/bin/env node
2
+ // toldya — stop repeating yourself to your AI.
3
+ //
4
+ // Reads what you typed to Claude Code (from its own history on this machine),
5
+ // finds the corrections you keep repeating, offers each one as a line for the
6
+ // rule file your agent reads, and next time counts whether you still say it.
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
8
+ import { spawn } from 'node:child_process';
9
+ import { join, resolve } from 'node:path';
10
+ import { homedir, tmpdir } from 'node:os';
11
+ import { createInterface } from 'node:readline/promises';
12
+ import {
13
+ CLAUDE_ROOT, claudeProjects, projectFor, readClaudeSession,
14
+ corrections, group, repeats, toRule, alreadyWritten, beforeAfter, isRetry,
15
+ } from '../lib/core.mjs';
16
+ import { cardHtml } from '../lib/card.mjs';
17
+
18
+ const VERSION = '0.3.1';
19
+ const argv = process.argv.slice(2);
20
+ const has = (f) => argv.includes(f);
21
+ const opt = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
22
+
23
+ if (has('--help') || has('-h')) {
24
+ console.log(`toldya ${VERSION} — stop repeating yourself to your AI
25
+
26
+ toldya this project: what you keep telling Claude Code here
27
+ toldya --all every project; rules go to your global ~/.claude/CLAUDE.md
28
+ --min N only repeats said at least N times (default 3)
29
+ --to FILE write rules to FILE instead (e.g. AGENTS.md)
30
+ --add 1,3 add repeats by their number, without asking
31
+ --dry show the report, change nothing
32
+ --card make an image of your top repeats, to share
33
+ --json machine-readable report, change nothing
34
+
35
+ Reads Claude Code's own history on this machine. Sends nothing anywhere.`);
36
+ process.exit(0);
37
+ }
38
+ if (has('--version') || has('-v')) { console.log(VERSION); process.exit(0); }
39
+ if (argv.some((a) => /^[\w.-]+\/[\w.-]+$/.test(a))) {
40
+ console.log('Team mode (owner/repo) is coming next. For now: run `npx toldya` in your project.');
41
+ process.exit(0);
42
+ }
43
+
44
+ const all = has('--all');
45
+ const min = Math.max(2, parseInt(opt('--min', '3'), 10) || 3);
46
+ const cwd = process.cwd();
47
+ const projects = claudeProjects();
48
+ const chosen = all ? projects : [projectFor(cwd, projects)].filter(Boolean);
49
+
50
+ if (!chosen.length) {
51
+ console.log(projects.length
52
+ ? `No Claude Code history for this folder (${cwd}).\nRun it inside a project you've used Claude Code in, or try: npx toldya --all`
53
+ : `No Claude Code history found at ${CLAUDE_ROOT}.`);
54
+ process.exit(0);
55
+ }
56
+
57
+ // Everything you typed, stamped with the session it came from.
58
+ const messages = [];
59
+ let sessions = 0;
60
+ for (const p of chosen) {
61
+ const dir = join(CLAUDE_ROOT, p);
62
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsonl'))) {
63
+ const ms = readClaudeSession(join(dir, f));
64
+ if (ms.length) sessions++;
65
+ for (const m of ms) messages.push({ ...m, session: `${p}/${f}` });
66
+ }
67
+ }
68
+ const said = corrections(messages);
69
+ const retries = said.filter((i) => isRetry(i.s)).length;
70
+ const items = said.filter((i) => !isRetry(i.s));
71
+ const found = repeats(group(items), min);
72
+
73
+ // Where rules go, and what's already written there.
74
+ const target = resolve(opt('--to', all ? join(homedir(), '.claude', 'CLAUDE.md') : join(cwd, 'CLAUDE.md')));
75
+ const ruleFiles = [...new Set(all ? [target] : [target, join(cwd, 'CLAUDE.md'), join(cwd, 'AGENTS.md')])];
76
+ const written = ruleFiles.filter(existsSync).map((f) => readFileSync(f, 'utf8')).join('\n');
77
+
78
+ // Rules toldya added before, so we can count whether they worked.
79
+ const stateDir = join(homedir(), '.toldya');
80
+ const stateFile = join(stateDir, 'state.json');
81
+ // Rules are remembered per rule file: that's where they live.
82
+ const key = target;
83
+ let state = {};
84
+ try { state = JSON.parse(readFileSync(stateFile, 'utf8')); } catch {}
85
+ const ours = state[key]?.rules || [];
86
+
87
+ const dates = messages.map((m) => m.ts).filter(Boolean).sort();
88
+ const day = (t) => (t ? new Date(t).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '?');
89
+
90
+ if (has('--json')) {
91
+ console.log(JSON.stringify({
92
+ sessions, messages: messages.length, corrections: items.length, retries,
93
+ from: dates[0] || null, to: dates.at(-1) || null,
94
+ repeats: found.map((r) => ({ phrase: r.phrase, count: r.count, sessions: r.sessions,
95
+ alreadyWritten: alreadyWritten(r.phrase, written) })),
96
+ rules: ours.map((r) => ({ ...r, ...beforeAfter(r, items) })),
97
+ }, null, 2));
98
+ process.exit(0);
99
+ }
100
+
101
+ console.log(`\ntoldya · ${sessions} sessions (${day(dates[0])} – ${day(dates.at(-1))}) · ${messages.length} of your messages · ${items.length} corrections\n`);
102
+
103
+ if (ours.length) {
104
+ console.log('Rules toldya added earlier:');
105
+ for (const r of ours) {
106
+ const { before, after } = beforeAfter(r, items);
107
+ console.log(` "${r.text}" said ${before}× before · ${after}× since (added ${day(r.addedAt)})`);
108
+ }
109
+ console.log('');
110
+ }
111
+
112
+ const fresh = found.filter((r) => !ours.some((o) => o.text === toRule(r.phrase)));
113
+ if (!fresh.length) {
114
+ console.log(found.length ? 'Nothing new you keep repeating. Nice.' : `No correction said ${min}+ times across sessions yet.`);
115
+ process.exit(0);
116
+ }
117
+
118
+ if (has('--card')) {
119
+ if (!found.length) { console.log(`No correction said ${min}+ times yet, so no card.`); process.exit(0); }
120
+ const file = join(tmpdir(), 'toldya-card.html');
121
+ writeFileSync(file, cardHtml({ repeats: found, sessions, from: day(dates[0]), to: day(dates.at(-1)) }));
122
+ const open = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', file]]
123
+ : [process.platform === 'darwin' ? 'open' : 'xdg-open', [file]];
124
+ try { spawn(open[0], open[1], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref(); } catch {}
125
+ console.log(`Your card: ${file}
126
+ It opens in your browser: save it as a PNG, then post it. Nothing leaves your machine unless you click share.`);
127
+ process.exit(0);
128
+ }
129
+
130
+ const STAR = '\nFound something? A star helps other people find toldya: github.com/singhlabsdev/toldya';
131
+ console.log('You keep telling your AI:');
132
+ const shown = fresh.slice(0, 10);
133
+ shown.forEach((r, n) => {
134
+ const note = alreadyWritten(r.phrase, written) ? ' ← already in your rules, still repeated' : '';
135
+ console.log(` ${String(n + 1).padStart(2)}. ${String(r.count).padStart(3)}× ${r.phrase} (${r.sessions} sessions)${note}`);
136
+ });
137
+ if (retries >= min) console.log(`\nAnd ${retries} times you told it to try or check again: its first go missed.`);
138
+ const spanDays = dates.length ? (new Date(dates.at(-1)) - new Date(dates[0])) / 864e5 : 0;
139
+ if (spanDays < 35) console.log('\nClaude Code keeps about 30 days of history by default, so older repeats are not counted.');
140
+
141
+ const picks = opt('--add', null);
142
+ if (has('--dry') || (!picks && !process.stdin.isTTY)) {
143
+ if (!has('--dry')) console.log('\nRun in a terminal to add these as rules, or pick them with --add 1,3.');
144
+ console.log(STAR);
145
+ process.exit(0);
146
+ }
147
+
148
+ // Ask before touching any file (or take the numbers given). Each rule is your own words.
149
+ const add = [];
150
+ if (picks) {
151
+ for (const n of String(picks).split(',').map((x) => parseInt(x, 10))) {
152
+ const r = shown[n - 1];
153
+ if (r && !alreadyWritten(r.phrase, written)) add.push(toRule(r.phrase));
154
+ }
155
+ } else {
156
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
157
+ console.log(`\nAdd them to ${target}? For each: y = add, n = skip, e = edit the wording.\n`);
158
+ for (const r of shown) {
159
+ if (alreadyWritten(r.phrase, written)) continue;
160
+ let text = toRule(r.phrase);
161
+ const a = (await rl.question(` ${text} [y/n/e] `)).trim().toLowerCase();
162
+ if (a === 'e') text = toRule((await rl.question(' new wording: ')).trim() || r.phrase);
163
+ if (a === 'y' || a === 'e') add.push(text);
164
+ }
165
+ rl.close();
166
+ }
167
+
168
+ if (!add.length) { console.log('\nNothing added.'); process.exit(0); }
169
+
170
+ const heading = '## Things I kept repeating';
171
+ let doc = existsSync(target) ? readFileSync(target, 'utf8') : '';
172
+ const block = add.map((t) => `- ${t}`).join('\n');
173
+ if (doc.includes(heading)) doc = doc.replace(heading, `${heading}\n${block}`);
174
+ else doc = `${doc.replace(/\s*$/, '')}${doc ? '\n\n' : ''}${heading}\n${block}\n`;
175
+ mkdirSync(join(target, '..'), { recursive: true });
176
+ writeFileSync(target, doc);
177
+
178
+ const now = new Date().toISOString();
179
+ state[key] = { rules: [...ours, ...add.map((text) => ({ text, addedAt: now }))] };
180
+ mkdirSync(stateDir, { recursive: true });
181
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
182
+ const shortTarget = target.startsWith(cwd) ? target.slice(cwd.length + 1) : target.replace(homedir(), '~');
183
+ console.log(`\nAdded ${add.length} rule${add.length > 1 ? 's' : ''} to ${shortTarget}. Run toldya again in a week to see if they stuck.`);
184
+ console.log(STAR);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toldya",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Stop repeating yourself to your AI. Finds what you keep telling your coding agent, writes it into CLAUDE.md / AGENTS.md, and counts whether it stopped.",
5
5
  "bin": {
6
6
  "toldya": "bin/toldya.mjs"