toldya 0.3.1 → 0.3.2
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 +100 -90
- package/bin/toldya.mjs +184 -184
- package/lib/card.mjs +73 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,90 +1,100 @@
|
|
|
1
|
-
<p align="center"><img src="assets/mark.webp" width="220" height="220" alt="A round green figure sits against a wall covered in chalk tally marks, adding one more, looking straight at you."></p>
|
|
2
|
-
|
|
3
|
-
<h1 align="center">toldya</h1>
|
|
4
|
-
|
|
5
|
-
<p align="center"><b>Stop repeating yourself to your AI.</b></p>
|
|
6
|
-
|
|
7
|
-
<p align="center">Part of <a href="https://singhlabs.dev/toldya/">Singh Labs</a>, small tools for people who code with AI.</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="https://www.npmjs.com/package/toldya"><img src="https://img.shields.io/npm/v/toldya?color=ffd23f&label=npm" alt="npm version"></a>
|
|
11
|
-
<img src="https://img.shields.io/badge/dependencies-0-98a179" alt="zero dependencies">
|
|
12
|
-
<img src="https://img.shields.io/badge/network-none-98a179" alt="no network calls">
|
|
13
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-ff90e8" alt="MIT"></a>
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
<p align="center"><img src="assets/demo.gif" width="100%" alt="toldya reading Claude Code history, listing the corrections said most often, then adding three of them to CLAUDE.md"></p>
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npx toldya --all
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
You keep telling your coding agent the same things. "Keep it simple." "Don't assume."
|
|
23
|
-
"Don't complicate it." toldya reads what you typed to Claude Code, finds what you keep
|
|
24
|
-
repeating, and writes it into the rule file your agent reads, with your yes. Next time,
|
|
25
|
-
it counts whether you still had to say it.
|
|
26
|
-
|
|
27
|
-
## What it found on the author's own history
|
|
28
|
-
|
|
29
|
-
A real run. Not a mock-up.
|
|
30
|
-
|
|
31
|
-
<p align="center"><img src="assets/report.png" width="100%" alt="toldya report: keep it simple 40 times in 36 sessions, dont complex this 29 times, dont assume 15 times, and 71 times it was told to try or check again"></p>
|
|
32
|
-
|
|
33
|
-
142 sessions. "Keep it simple" 40 times. Rewordings and typos count as one habit: "Don't
|
|
34
|
-
complicate it" and "dont complex this" are the same thing said twice.
|
|
35
|
-
|
|
36
|
-
## Use it
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npx toldya # this project: offers rules for ./CLAUDE.md
|
|
40
|
-
npx toldya --all # every project: offers rules for ~/.claude/CLAUDE.md
|
|
41
|
-
npx toldya --dry # report only, change nothing
|
|
42
|
-
npx toldya --to AGENTS.md # write rules somewhere else
|
|
43
|
-
npx toldya --add 1,3 # take repeats 1 and 3 without the prompts
|
|
44
|
-
npx toldya --min 5 # only things you said 5+ times
|
|
45
|
-
npx toldya --json # machine-readable report
|
|
46
|
-
npx toldya --card # an image of your top repeats, to share
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Without `--dry` or `--add`, it asks about each repeat: **y** adds it, **n** skips it,
|
|
50
|
-
**e** lets you reword it first. Rules go under a `## Things I kept repeating` heading.
|
|
51
|
-
Nothing is written without a yes.
|
|
52
|
-
|
|
53
|
-
Run it again a week later and every rule it added shows how often you said it before,
|
|
54
|
-
and how often since.
|
|
55
|
-
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
<p align="center"><img src="assets/mark.webp" width="220" height="220" alt="A round green figure sits against a wall covered in chalk tally marks, adding one more, looking straight at you."></p>
|
|
2
|
+
|
|
3
|
+
<h1 align="center">toldya</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center"><b>Stop repeating yourself to your AI.</b></p>
|
|
6
|
+
|
|
7
|
+
<p align="center">Part of <a href="https://singhlabs.dev/toldya/">Singh Labs</a>, small tools for people who code with AI.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/toldya"><img src="https://img.shields.io/npm/v/toldya?color=ffd23f&label=npm" alt="npm version"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/dependencies-0-98a179" alt="zero dependencies">
|
|
12
|
+
<img src="https://img.shields.io/badge/network-none-98a179" alt="no network calls">
|
|
13
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-ff90e8" alt="MIT"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center"><img src="assets/demo.gif" width="100%" alt="toldya reading Claude Code history, listing the corrections said most often, then adding three of them to CLAUDE.md"></p>
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx toldya --all
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You keep telling your coding agent the same things. "Keep it simple." "Don't assume."
|
|
23
|
+
"Don't complicate it." toldya reads what you typed to Claude Code, finds what you keep
|
|
24
|
+
repeating, and writes it into the rule file your agent reads, with your yes. Next time,
|
|
25
|
+
it counts whether you still had to say it.
|
|
26
|
+
|
|
27
|
+
## What it found on the author's own history
|
|
28
|
+
|
|
29
|
+
A real run. Not a mock-up.
|
|
30
|
+
|
|
31
|
+
<p align="center"><img src="assets/report.png" width="100%" alt="toldya report: keep it simple 40 times in 36 sessions, dont complex this 29 times, dont assume 15 times, and 71 times it was told to try or check again"></p>
|
|
32
|
+
|
|
33
|
+
142 sessions. "Keep it simple" 40 times. Rewordings and typos count as one habit: "Don't
|
|
34
|
+
complicate it" and "dont complex this" are the same thing said twice.
|
|
35
|
+
|
|
36
|
+
## Use it
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx toldya # this project: offers rules for ./CLAUDE.md
|
|
40
|
+
npx toldya --all # every project: offers rules for ~/.claude/CLAUDE.md
|
|
41
|
+
npx toldya --dry # report only, change nothing
|
|
42
|
+
npx toldya --to AGENTS.md # write rules somewhere else
|
|
43
|
+
npx toldya --add 1,3 # take repeats 1 and 3 without the prompts
|
|
44
|
+
npx toldya --min 5 # only things you said 5+ times
|
|
45
|
+
npx toldya --json # machine-readable report
|
|
46
|
+
npx toldya --card # an image of your top repeats, to share
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Without `--dry` or `--add`, it asks about each repeat: **y** adds it, **n** skips it,
|
|
50
|
+
**e** lets you reword it first. Rules go under a `## Things I kept repeating` heading.
|
|
51
|
+
Nothing is written without a yes.
|
|
52
|
+
|
|
53
|
+
Run it again a week later and every rule it added shows how often you said it before,
|
|
54
|
+
and how often since.
|
|
55
|
+
|
|
56
|
+
## In Claude Code, as a plugin
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
/plugin marketplace add singhlabsdev/toldya
|
|
60
|
+
/plugin install toldya@toldya
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then ask Claude *"what do I keep repeating?"*. It runs toldya, shows you the list, and adds
|
|
64
|
+
only the ones you pick.
|
|
65
|
+
|
|
66
|
+
## Share it
|
|
67
|
+
|
|
68
|
+
`npx toldya --all --card` opens your card in the browser. Save it as a PNG, then use the
|
|
69
|
+
**Post on X** or **Share on LinkedIn** button: the post text is written for you. The card is
|
|
70
|
+
drawn on your machine; nothing leaves it unless you click share.
|
|
71
|
+
|
|
72
|
+
Post yours in [Share your card](https://github.com/singhlabsdev/toldya/discussions/1). What's your top line?
|
|
73
|
+
|
|
74
|
+
<p align="center"><img src="assets/card.png" width="100%" alt="toldya card: I have told my AI keep it simple 42 times. Also: dont complex this 29, dont assume 15, i dont want later on 13"></p>
|
|
75
|
+
|
|
76
|
+
## What it reads, and what it doesn't
|
|
77
|
+
|
|
78
|
+
- Only **your own messages** in Claude Code's history on this machine (`~/.claude/projects`).
|
|
79
|
+
Not the AI's replies, not tool output, not your files.
|
|
80
|
+
- Long messages are treated as pastes (briefs, logs) and skipped. Questions and "I don't
|
|
81
|
+
understand" are not corrections, so they're skipped too.
|
|
82
|
+
- A repeat counts only if it shows up in **at least two sessions**.
|
|
83
|
+
- Short "try again" / "check again" messages are counted separately. They mean the first
|
|
84
|
+
attempt missed, but they aren't rules you can write down.
|
|
85
|
+
- **Sends nothing anywhere.** No account, no telemetry, no network calls.
|
|
86
|
+
|
|
87
|
+
Claude Code keeps about 30 days of history by default, so on a default install toldya sees
|
|
88
|
+
roughly a month.
|
|
89
|
+
|
|
90
|
+
## Coming next
|
|
91
|
+
|
|
92
|
+
- Codex history (not tested on real files yet, so not claimed).
|
|
93
|
+
|
|
94
|
+
## Requirements
|
|
95
|
+
|
|
96
|
+
Node 18+. No dependencies.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT © Sevenopt Solutions Private Limited · made by [Singh Labs](https://singhlabs.dev)
|
package/bin/toldya.mjs
CHANGED
|
@@ -1,184 +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.
|
|
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('
|
|
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);
|
|
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.2';
|
|
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('toldya reads your own Claude Code history, not a repo. 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/lib/card.mjs
CHANGED
|
@@ -1,53 +1,73 @@
|
|
|
1
|
-
// toldya --card: one image of your top repeats, for when you want to show someone.
|
|
2
|
-
// Writes a local HTML page that draws the card and saves it as a PNG. No network,
|
|
3
|
-
// unless you click a share button: that opens X or LinkedIn with a pre-filled post.
|
|
4
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
-
|
|
6
|
-
const MARK = new URL('../assets/mark.webp', import.meta.url);
|
|
7
|
-
|
|
8
|
-
export function cardHtml({ repeats, sessions, from, to }) {
|
|
9
|
-
let mark = '';
|
|
10
|
-
try { mark = 'data:image/webp;base64,' + readFileSync(MARK).toString('base64'); } catch {}
|
|
11
|
-
const data = { rows: repeats.slice(0, 5).map((r) => [r.count, r.phrase]), sessions, from, to, mark };
|
|
12
|
-
// JSON inside <script>: escape "<" so a phrase like "</script>" can't end the block.
|
|
13
|
-
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
14
|
-
return `<!doctype html><meta charset="utf-8"><title>toldya card</title>
|
|
15
|
-
<style>body{margin:0;background:#eee;font:16px system-ui;display:grid;place-items:center;min-height:100vh;gap:16px}
|
|
16
|
-
canvas{max-width:95vw;height:auto;box-shadow:0 2px 12px #0002}.row{display:flex;gap:14px;flex-wrap:wrap;justify-content:center}p{margin:0;color:#555;max-width:60ch;text-align:center}a{font-weight:700;color:#0d0d0d;background:#ffd23f;border:3px solid #0d0d0d;padding:10px 18px;border-radius:30px;text-decoration:none;box-shadow:4px 4px 0 #0d0d0d}</style>
|
|
17
|
-
<canvas id="c" width="1200" height="675"></canvas>
|
|
18
|
-
<div class="row"><a id="save" download="toldya.png" href="#">1. Save as PNG</a><a id="x" target="_blank" rel="noopener" href="#">2. Post on X</a><a id="li" target="_blank" rel="noopener" href="#">2. Share on LinkedIn</a></div>
|
|
19
|
-
<p>Save the picture first, then attach it to your post. The card is drawn on your machine; nothing leaves it unless you click a share button.</p>
|
|
20
|
-
<script>
|
|
21
|
-
const D = ${json}, c = document.getElementById('c'), x = c.getContext('2d'), INK = '#0d0d0d';
|
|
22
|
-
const REPO = 'github.com/singhlabsdev/toldya';
|
|
23
|
-
const first = D.rows[0] || [0, ''];
|
|
24
|
-
const post = 'My AI has heard "' + first[1] + '" ' + first[0] + ' times 😅 Counted from my own Claude Code history with npx toldya';
|
|
25
|
-
document.getElementById('x').href = 'https://x.com/intent/post?text=' + encodeURIComponent(post) + '&url=' + encodeURIComponent('https://' + REPO);
|
|
26
|
-
document.getElementById('li').href = 'https://www.linkedin.com/feed/?shareActive=true&text=' + encodeURIComponent(post + ' https://' + REPO);
|
|
27
|
-
const font = (w, s) => (x.font = w + ' ' + s + 'px system-ui, "Segoe UI", sans-serif');
|
|
28
|
-
const pill = (px, py, w, h, fill) => {
|
|
29
|
-
x.fillStyle = INK; x.beginPath(); x.roundRect(px + 5, py + 5, w, h, h / 2); x.fill();
|
|
30
|
-
x.fillStyle = fill; x.beginPath(); x.roundRect(px, py, w, h, h / 2); x.fill();
|
|
31
|
-
x.lineWidth = 3; x.strokeStyle = INK; x.stroke();
|
|
32
|
-
};
|
|
33
|
-
const fit = (s, max) => { if (x.measureText(s).width <= max) return s;
|
|
34
|
-
while (s.length > 1 && x.measureText(s + '…').width > max) s = s.slice(0, -1); return s + '…'; };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
// toldya --card: one image of your top repeats, for when you want to show someone.
|
|
2
|
+
// Writes a local HTML page that draws the card and saves it as a PNG. No network,
|
|
3
|
+
// unless you click a share button: that opens X or LinkedIn with a pre-filled post.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const MARK = new URL('../assets/mark.webp', import.meta.url);
|
|
7
|
+
|
|
8
|
+
export function cardHtml({ repeats, sessions, from, to }) {
|
|
9
|
+
let mark = '';
|
|
10
|
+
try { mark = 'data:image/webp;base64,' + readFileSync(MARK).toString('base64'); } catch {}
|
|
11
|
+
const data = { rows: repeats.slice(0, 5).map((r) => [r.count, r.phrase]), sessions, from, to, mark };
|
|
12
|
+
// JSON inside <script>: escape "<" so a phrase like "</script>" can't end the block.
|
|
13
|
+
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
14
|
+
return `<!doctype html><meta charset="utf-8"><title>toldya card</title>
|
|
15
|
+
<style>body{margin:0;background:#eee;font:16px system-ui;display:grid;place-items:center;min-height:100vh;gap:16px}
|
|
16
|
+
canvas{max-width:95vw;height:auto;box-shadow:0 2px 12px #0002}.row{display:flex;gap:14px;flex-wrap:wrap;justify-content:center}p{margin:0;color:#555;max-width:60ch;text-align:center}a{font-weight:700;color:#0d0d0d;background:#ffd23f;border:3px solid #0d0d0d;padding:10px 18px;border-radius:30px;text-decoration:none;box-shadow:4px 4px 0 #0d0d0d}</style>
|
|
17
|
+
<canvas id="c" width="1200" height="675"></canvas>
|
|
18
|
+
<div class="row"><a id="save" download="toldya.png" href="#">1. Save as PNG</a><a id="x" target="_blank" rel="noopener" href="#">2. Post on X</a><a id="li" target="_blank" rel="noopener" href="#">2. Share on LinkedIn</a></div>
|
|
19
|
+
<p>Save the picture first, then attach it to your post. The card is drawn on your machine; nothing leaves it unless you click a share button. Or add it to <a style="all:revert" href="https://github.com/singhlabsdev/toldya/discussions/1" target="_blank" rel="noopener">Share your card</a> on GitHub.</p>
|
|
20
|
+
<script>
|
|
21
|
+
const D = ${json}, c = document.getElementById('c'), x = c.getContext('2d'), INK = '#0d0d0d';
|
|
22
|
+
const REPO = 'github.com/singhlabsdev/toldya';
|
|
23
|
+
const first = D.rows[0] || [0, ''];
|
|
24
|
+
const post = 'My AI has heard "' + first[1] + '" ' + first[0] + ' times 😅 Counted from my own Claude Code history with npx toldya';
|
|
25
|
+
document.getElementById('x').href = 'https://x.com/intent/post?text=' + encodeURIComponent(post) + '&url=' + encodeURIComponent('https://' + REPO);
|
|
26
|
+
document.getElementById('li').href = 'https://www.linkedin.com/feed/?shareActive=true&text=' + encodeURIComponent(post + ' https://' + REPO);
|
|
27
|
+
const font = (w, s) => (x.font = w + ' ' + s + 'px system-ui, "Segoe UI", sans-serif');
|
|
28
|
+
const pill = (px, py, w, h, fill) => {
|
|
29
|
+
x.fillStyle = INK; x.beginPath(); x.roundRect(px + 5, py + 5, w, h, h / 2); x.fill();
|
|
30
|
+
x.fillStyle = fill; x.beginPath(); x.roundRect(px, py, w, h, h / 2); x.fill();
|
|
31
|
+
x.lineWidth = 3; x.strokeStyle = INK; x.stroke();
|
|
32
|
+
};
|
|
33
|
+
const fit = (s, max) => { if (x.measureText(s).width <= max) return s;
|
|
34
|
+
while (s.length > 1 && x.measureText(s + '…').width > max) s = s.slice(0, -1); return s + '…'; };
|
|
35
|
+
// Wrap s into at most 2 lines of width max; the second line gets an ellipsis if it overflows.
|
|
36
|
+
const wrap = (s, max) => {
|
|
37
|
+
const lines = [''];
|
|
38
|
+
for (const w of s.split(' ')) {
|
|
39
|
+
const t = lines.at(-1) ? lines.at(-1) + ' ' + w : w;
|
|
40
|
+
if (x.measureText(t).width <= max || !lines.at(-1)) lines[lines.length - 1] = t;
|
|
41
|
+
else if (lines.length < 2) lines.push(w);
|
|
42
|
+
else { lines[1] = fit(lines[1] + ' ' + w, max); break; }
|
|
43
|
+
}
|
|
44
|
+
return lines;
|
|
45
|
+
};
|
|
46
|
+
function draw(img) {
|
|
47
|
+
x.fillStyle = '#fff'; x.fillRect(0, 0, 1200, 675);
|
|
48
|
+
if (img) x.drawImage(img, 20, 170, 330, 330);
|
|
49
|
+
// The headline is the top correction itself, in quotes, with its count.
|
|
50
|
+
x.fillStyle = '#6b6b6b'; font(600, 30); x.fillText('I have told my AI', 390, 92);
|
|
51
|
+
x.fillStyle = INK; font(800, 66);
|
|
52
|
+
const lines = wrap('“' + first[1] + '”', 760);
|
|
53
|
+
lines.forEach((l, i) => x.fillText(l, 390, 170 + i * 76));
|
|
54
|
+
const cy = 170 + (lines.length - 1) * 76 + 34;
|
|
55
|
+
pill(390, cy, 250, 72, '#ffd23f');
|
|
56
|
+
x.fillStyle = INK; font(800, 44); x.textAlign = 'center'; x.fillText(first[0] + ' times', 515, cy + 52); x.textAlign = 'left';
|
|
57
|
+
// The rest, smaller.
|
|
58
|
+
const rest = D.rows.slice(1, 4);
|
|
59
|
+
if (rest.length) { x.fillStyle = '#6b6b6b'; font(600, 24); x.fillText('also:', 390, cy + 128); }
|
|
60
|
+
rest.forEach(([n, p], i) => {
|
|
61
|
+
const y = cy + 128 + (i + 1) * 40;
|
|
62
|
+
x.fillStyle = INK; font(800, 26); x.fillText(n + '×', 390, y);
|
|
63
|
+
font(500, 26); x.fillText(fit(p, 640), 470, y);
|
|
64
|
+
});
|
|
65
|
+
font(500, 22); x.fillStyle = '#6b6b6b';
|
|
66
|
+
x.fillText(D.sessions + ' Claude Code sessions · ' + D.from + ' – ' + D.to, 390, 628);
|
|
67
|
+
pill(990, 596, 180, 50, INK); x.fillStyle = '#fff'; font(700, 24); x.fillText('npx toldya', 1017, 629);
|
|
68
|
+
x.fillStyle = '#6b6b6b'; font(500, 18); x.textAlign = 'right'; x.fillText(REPO, 1170, 580); x.textAlign = 'left';
|
|
69
|
+
document.getElementById('save').href = c.toDataURL('image/png');
|
|
70
|
+
}
|
|
71
|
+
if (D.mark) { const i = new Image(); i.onload = () => draw(i); i.onerror = () => draw(); i.src = D.mark; } else draw();
|
|
72
|
+
</script>`;
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toldya",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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"
|