toldya 0.0.1 → 0.1.0
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 +70 -16
- package/bin/toldya.mjs +166 -0
- package/lib/core.mjs +171 -0
- package/package.json +7 -3
- package/bin/toldya.js +0 -19
package/README.md
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
|
-
|
|
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
2
|
|
|
3
|
-
|
|
3
|
+
<h1 align="center">toldya</h1>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
"Run the tests before you say it's done." toldya finds what you keep repeating, writes it into
|
|
7
|
-
the rule file your agent reads (`CLAUDE.md` / `AGENTS.md`), then counts whether it stopped.
|
|
5
|
+
<p align="center"><b>Stop repeating yourself to your AI.</b></p>
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<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>
|
|
9
|
+
<img src="https://img.shields.io/badge/dependencies-0-98a179" alt="zero dependencies">
|
|
10
|
+
<img src="https://img.shields.io/badge/network-none-98a179" alt="no network calls">
|
|
11
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-ff90e8" alt="MIT"></a>
|
|
12
|
+
</p>
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
writes nothing. **v0.1** is being built.
|
|
14
|
+
<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>
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npx toldya --all
|
|
18
|
+
```
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **Count:** next run shows how often each repeat came back after the rule was added.
|
|
20
|
+
You keep telling your coding agent the same things. "Keep it simple." "Don't assume."
|
|
21
|
+
"Don't complicate it." toldya reads what you typed to Claude Code, finds what you keep
|
|
22
|
+
repeating, and writes it into the rule file your agent reads, with your yes. Next time,
|
|
23
|
+
it counts whether you still had to say it.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## What it found on the author's own history
|
|
26
|
+
|
|
27
|
+
A real run. Not a mock-up.
|
|
28
|
+
|
|
29
|
+
<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>
|
|
30
|
+
|
|
31
|
+
142 sessions. "Keep it simple" 40 times. Rewordings and typos count as one habit: "Don't
|
|
32
|
+
complicate it" and "dont complex this" are the same thing said twice.
|
|
33
|
+
|
|
34
|
+
## Use it
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx toldya # this project: offers rules for ./CLAUDE.md
|
|
38
|
+
npx toldya --all # every project: offers rules for ~/.claude/CLAUDE.md
|
|
39
|
+
npx toldya --dry # report only, change nothing
|
|
40
|
+
npx toldya --to AGENTS.md # write rules somewhere else
|
|
41
|
+
npx toldya --add 1,3 # take repeats 1 and 3 without the prompts
|
|
42
|
+
npx toldya --min 5 # only things you said 5+ times
|
|
43
|
+
npx toldya --json # machine-readable report
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without `--dry` or `--add`, it asks about each repeat: **y** adds it, **n** skips it,
|
|
47
|
+
**e** lets you reword it first. Rules go under a `## Things I kept repeating` heading.
|
|
48
|
+
Nothing is written without a yes.
|
|
49
|
+
|
|
50
|
+
Run it again a week later and every rule it added shows how often you said it before,
|
|
51
|
+
and how often since.
|
|
52
|
+
|
|
53
|
+
## What it reads, and what it doesn't
|
|
54
|
+
|
|
55
|
+
- Only **your own messages** in Claude Code's history on this machine (`~/.claude/projects`).
|
|
56
|
+
Not the AI's replies, not tool output, not your files.
|
|
57
|
+
- Long messages are treated as pastes (briefs, logs) and skipped. Questions and "I don't
|
|
58
|
+
understand" are not corrections, so they're skipped too.
|
|
59
|
+
- A repeat counts only if it shows up in **at least two sessions**.
|
|
60
|
+
- Short "try again" / "check again" messages are counted separately. They mean the first
|
|
61
|
+
attempt missed, but they aren't rules you can write down.
|
|
62
|
+
- **Sends nothing anywhere.** No account, no telemetry, no network calls.
|
|
63
|
+
|
|
64
|
+
Claude Code keeps about 30 days of history by default, so on a default install toldya sees
|
|
65
|
+
roughly a month.
|
|
66
|
+
|
|
67
|
+
## Coming next
|
|
68
|
+
|
|
69
|
+
- **Team mode:** `npx toldya owner/repo` reads a repo's pull-request review comments (from
|
|
70
|
+
people or any review bot), finds what reviewers keep writing, and opens one PR into `AGENTS.md`.
|
|
71
|
+
- **A share card:** one image of your top repeats, for when you want to show someone.
|
|
72
|
+
- Codex history (not tested on real files yet, so not claimed).
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
Node 18+. No dependencies.
|
|
23
77
|
|
|
24
78
|
## License
|
|
25
79
|
|
|
26
|
-
MIT © Sevenopt Solutions Private Limited
|
|
80
|
+
MIT © Sevenopt Solutions Private Limited · made by [Singh Labs](https://singhlabs.dev)
|
package/bin/toldya.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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 { join, resolve } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import {
|
|
12
|
+
CLAUDE_ROOT, claudeProjects, projectFor, readClaudeSession,
|
|
13
|
+
corrections, group, repeats, toRule, alreadyWritten, beforeAfter, isRetry,
|
|
14
|
+
} from '../lib/core.mjs';
|
|
15
|
+
|
|
16
|
+
const VERSION = '0.1.0';
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const has = (f) => argv.includes(f);
|
|
19
|
+
const opt = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
|
|
20
|
+
|
|
21
|
+
if (has('--help') || has('-h')) {
|
|
22
|
+
console.log(`toldya ${VERSION} — stop repeating yourself to your AI
|
|
23
|
+
|
|
24
|
+
toldya this project: what you keep telling Claude Code here
|
|
25
|
+
toldya --all every project; rules go to your global ~/.claude/CLAUDE.md
|
|
26
|
+
--min N only repeats said at least N times (default 3)
|
|
27
|
+
--to FILE write rules to FILE instead (e.g. AGENTS.md)
|
|
28
|
+
--add 1,3 add repeats by their number, without asking
|
|
29
|
+
--dry show the report, change nothing
|
|
30
|
+
--json machine-readable report, change nothing
|
|
31
|
+
|
|
32
|
+
Reads Claude Code's own history on this machine. Sends nothing anywhere.`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
if (has('--version') || has('-v')) { console.log(VERSION); process.exit(0); }
|
|
36
|
+
if (argv.some((a) => /^[\w.-]+\/[\w.-]+$/.test(a))) {
|
|
37
|
+
console.log('Team mode (owner/repo) is coming next. For now: run `npx toldya` in your project.');
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const all = has('--all');
|
|
42
|
+
const min = Math.max(2, parseInt(opt('--min', '3'), 10) || 3);
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const projects = claudeProjects();
|
|
45
|
+
const chosen = all ? projects : [projectFor(cwd, projects)].filter(Boolean);
|
|
46
|
+
|
|
47
|
+
if (!chosen.length) {
|
|
48
|
+
console.log(projects.length
|
|
49
|
+
? `No Claude Code history for this folder (${cwd}).\nRun it inside a project you've used Claude Code in, or try: npx toldya --all`
|
|
50
|
+
: `No Claude Code history found at ${CLAUDE_ROOT}.`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Everything you typed, stamped with the session it came from.
|
|
55
|
+
const messages = [];
|
|
56
|
+
let sessions = 0;
|
|
57
|
+
for (const p of chosen) {
|
|
58
|
+
const dir = join(CLAUDE_ROOT, p);
|
|
59
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsonl'))) {
|
|
60
|
+
const ms = readClaudeSession(join(dir, f));
|
|
61
|
+
if (ms.length) sessions++;
|
|
62
|
+
for (const m of ms) messages.push({ ...m, session: `${p}/${f}` });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const said = corrections(messages);
|
|
66
|
+
const retries = said.filter((i) => isRetry(i.s)).length;
|
|
67
|
+
const items = said.filter((i) => !isRetry(i.s));
|
|
68
|
+
const found = repeats(group(items), min);
|
|
69
|
+
|
|
70
|
+
// Where rules go, and what's already written there.
|
|
71
|
+
const target = resolve(opt('--to', all ? join(homedir(), '.claude', 'CLAUDE.md') : join(cwd, 'CLAUDE.md')));
|
|
72
|
+
const ruleFiles = [...new Set(all ? [target] : [target, join(cwd, 'CLAUDE.md'), join(cwd, 'AGENTS.md')])];
|
|
73
|
+
const written = ruleFiles.filter(existsSync).map((f) => readFileSync(f, 'utf8')).join('\n');
|
|
74
|
+
|
|
75
|
+
// Rules toldya added before, so we can count whether they worked.
|
|
76
|
+
const stateDir = join(homedir(), '.toldya');
|
|
77
|
+
const stateFile = join(stateDir, 'state.json');
|
|
78
|
+
// Rules are remembered per rule file: that's where they live.
|
|
79
|
+
const key = target;
|
|
80
|
+
let state = {};
|
|
81
|
+
try { state = JSON.parse(readFileSync(stateFile, 'utf8')); } catch {}
|
|
82
|
+
const ours = state[key]?.rules || [];
|
|
83
|
+
|
|
84
|
+
const dates = messages.map((m) => m.ts).filter(Boolean).sort();
|
|
85
|
+
const day = (t) => (t ? new Date(t).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '?');
|
|
86
|
+
|
|
87
|
+
if (has('--json')) {
|
|
88
|
+
console.log(JSON.stringify({
|
|
89
|
+
sessions, messages: messages.length, corrections: items.length, retries,
|
|
90
|
+
from: dates[0] || null, to: dates.at(-1) || null,
|
|
91
|
+
repeats: found.map((r) => ({ phrase: r.phrase, count: r.count, sessions: r.sessions,
|
|
92
|
+
alreadyWritten: alreadyWritten(r.phrase, written) })),
|
|
93
|
+
rules: ours.map((r) => ({ ...r, ...beforeAfter(r, items) })),
|
|
94
|
+
}, null, 2));
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`\ntoldya · ${sessions} sessions (${day(dates[0])} – ${day(dates.at(-1))}) · ${messages.length} of your messages · ${items.length} corrections\n`);
|
|
99
|
+
|
|
100
|
+
if (ours.length) {
|
|
101
|
+
console.log('Rules toldya added earlier:');
|
|
102
|
+
for (const r of ours) {
|
|
103
|
+
const { before, after } = beforeAfter(r, items);
|
|
104
|
+
console.log(` "${r.text}" said ${before}× before · ${after}× since (added ${day(r.addedAt)})`);
|
|
105
|
+
}
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fresh = found.filter((r) => !ours.some((o) => o.text === toRule(r.phrase)));
|
|
110
|
+
if (!fresh.length) {
|
|
111
|
+
console.log(found.length ? 'Nothing new you keep repeating. Nice.' : `No correction said ${min}+ times across sessions yet.`);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('You keep telling your AI:');
|
|
116
|
+
const shown = fresh.slice(0, 10);
|
|
117
|
+
shown.forEach((r, n) => {
|
|
118
|
+
const note = alreadyWritten(r.phrase, written) ? ' ← already in your rules, still repeated' : '';
|
|
119
|
+
console.log(` ${String(n + 1).padStart(2)}. ${String(r.count).padStart(3)}× ${r.phrase} (${r.sessions} sessions)${note}`);
|
|
120
|
+
});
|
|
121
|
+
if (retries >= min) console.log(`\nAnd ${retries} times you told it to try or check again: its first go missed.`);
|
|
122
|
+
const spanDays = dates.length ? (new Date(dates.at(-1)) - new Date(dates[0])) / 864e5 : 0;
|
|
123
|
+
if (spanDays < 35) console.log('\nClaude Code keeps about 30 days of history by default, so older repeats are not counted.');
|
|
124
|
+
|
|
125
|
+
const picks = opt('--add', null);
|
|
126
|
+
if (has('--dry') || (!picks && !process.stdin.isTTY)) {
|
|
127
|
+
if (!has('--dry')) console.log('\nRun in a terminal to add these as rules, or pick them with --add 1,3.');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Ask before touching any file (or take the numbers given). Each rule is your own words.
|
|
132
|
+
const add = [];
|
|
133
|
+
if (picks) {
|
|
134
|
+
for (const n of String(picks).split(',').map((x) => parseInt(x, 10))) {
|
|
135
|
+
const r = shown[n - 1];
|
|
136
|
+
if (r && !alreadyWritten(r.phrase, written)) add.push(toRule(r.phrase));
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
140
|
+
console.log(`\nAdd them to ${target}? For each: y = add, n = skip, e = edit the wording.\n`);
|
|
141
|
+
for (const r of shown) {
|
|
142
|
+
if (alreadyWritten(r.phrase, written)) continue;
|
|
143
|
+
let text = toRule(r.phrase);
|
|
144
|
+
const a = (await rl.question(` ${text} [y/n/e] `)).trim().toLowerCase();
|
|
145
|
+
if (a === 'e') text = toRule((await rl.question(' new wording: ')).trim() || r.phrase);
|
|
146
|
+
if (a === 'y' || a === 'e') add.push(text);
|
|
147
|
+
}
|
|
148
|
+
rl.close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!add.length) { console.log('\nNothing added.'); process.exit(0); }
|
|
152
|
+
|
|
153
|
+
const heading = '## Things I kept repeating';
|
|
154
|
+
let doc = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
|
155
|
+
const block = add.map((t) => `- ${t}`).join('\n');
|
|
156
|
+
if (doc.includes(heading)) doc = doc.replace(heading, `${heading}\n${block}`);
|
|
157
|
+
else doc = `${doc.replace(/\s*$/, '')}${doc ? '\n\n' : ''}${heading}\n${block}\n`;
|
|
158
|
+
mkdirSync(join(target, '..'), { recursive: true });
|
|
159
|
+
writeFileSync(target, doc);
|
|
160
|
+
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
state[key] = { rules: [...ours, ...add.map((text) => ({ text, addedAt: now }))] };
|
|
163
|
+
mkdirSync(stateDir, { recursive: true });
|
|
164
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
165
|
+
const shortTarget = target.startsWith(cwd) ? target.slice(cwd.length + 1) : target.replace(homedir(), '~');
|
|
166
|
+
console.log(`\nAdded ${add.length} rule${add.length > 1 ? 's' : ''} to ${shortTarget}. Run toldya again in a week to see if they stuck.`);
|
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// toldya core: read what you told your AI, find what you keep repeating.
|
|
2
|
+
// No dependencies, no network. Everything here runs on your machine.
|
|
3
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
export const CLAUDE_ROOT = join(homedir(), '.claude', 'projects');
|
|
8
|
+
|
|
9
|
+
// Claude Code stores D:\My work\app as D--My-work-app: every non-alphanumeric
|
|
10
|
+
// character becomes one dash, never collapsed.
|
|
11
|
+
export const encodeProject = (dir) => dir.replace(/[^A-Za-z0-9]/g, '-');
|
|
12
|
+
|
|
13
|
+
export function claudeProjects(root = CLAUDE_ROOT) {
|
|
14
|
+
if (!existsSync(root)) return [];
|
|
15
|
+
return readdirSync(root).filter((d) => {
|
|
16
|
+
try { return statSync(join(root, d)).isDirectory(); } catch { return false; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The project folder for this directory: longest matching prefix, so running
|
|
21
|
+
// from a subfolder still finds the project it belongs to.
|
|
22
|
+
export function projectFor(cwd, all) {
|
|
23
|
+
const want = encodeProject(cwd).toLowerCase();
|
|
24
|
+
return all.filter((d) => want.startsWith(d.toLowerCase()))
|
|
25
|
+
.sort((a, b) => b.length - a.length)[0] || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Messages you typed, from one transcript. Skips tool results, system notes,
|
|
29
|
+
// compaction summaries and slash-command output: only your own words count.
|
|
30
|
+
export function readClaudeSession(file) {
|
|
31
|
+
let lines;
|
|
32
|
+
try { lines = readFileSync(file, 'utf8').split('\n'); } catch { return []; }
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (!line.trim()) continue;
|
|
36
|
+
let e;
|
|
37
|
+
try { e = JSON.parse(line); } catch { continue; }
|
|
38
|
+
if (e.type !== 'user' || e.isMeta || e.isCompactSummary || e.toolUseResult) continue;
|
|
39
|
+
let c = e.message?.content;
|
|
40
|
+
if (Array.isArray(c)) c = c.filter((b) => b.type === 'text').map((b) => b.text).join('\n');
|
|
41
|
+
if (typeof c !== 'string' || !c.trim()) continue;
|
|
42
|
+
if (/^\s*</.test(c) || /\[Request interrupted/.test(c)) continue;
|
|
43
|
+
out.push({ text: c, ts: e.timestamp || '' });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A correction is a short instruction aimed at the agent's behaviour.
|
|
49
|
+
const CUE = /\b(don'?t|dont|do not|never|stop|always|avoid|instead|i told you|i said|again|not like that|why did you|make sure|keep it|no need|must)\b/i;
|
|
50
|
+
// Not corrections: you being lost, or asking something.
|
|
51
|
+
const NOT = /\bunde\w*t[ae]?n|\bunders\w*|\b(don'?t know|dont know|no idea|what is|where is|how do|how to|where to)\b|\?\s*$/i;
|
|
52
|
+
// Long messages are pastes (specs, logs, briefs), not things you said.
|
|
53
|
+
const PASTE = 600;
|
|
54
|
+
|
|
55
|
+
// "Try again", "check again", "so try again": short sentences built on "again"
|
|
56
|
+
// are retries, not rules. They mean the first attempt missed. Counted
|
|
57
|
+
// separately, never offered as a rule.
|
|
58
|
+
export const isRetry = (s) => s.split(' ').length <= 5 && /\bagain\b/i.test(s);
|
|
59
|
+
|
|
60
|
+
export function corrections(messages) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const m of messages) {
|
|
63
|
+
if (m.text.length > PASTE) continue;
|
|
64
|
+
for (let s of m.text.split(/(?<=[.!?])\s+|\n+|\.{2,}|,{2,}/)) {
|
|
65
|
+
s = s.trim().replace(/\s+/g, ' ');
|
|
66
|
+
const words = s.split(' ').length;
|
|
67
|
+
if (words < 2 || words > 16) continue;
|
|
68
|
+
if (/https?:|\]\(|`/.test(s)) continue;
|
|
69
|
+
if (!CUE.test(s) || NOT.test(s)) continue;
|
|
70
|
+
out.push({ ...m, s });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const STOP = new Set("the a an to of and or is it i you me my we our this that for in on with be its it's please just so also now ok okay".split(' '));
|
|
77
|
+
// Words cut to their first five letters, so "complex" and "complicate",
|
|
78
|
+
// "simple" and "simpler", count as the same word.
|
|
79
|
+
const tokens = (s) => new Set((s.toLowerCase().match(/[a-z0-9']+/g) || [])
|
|
80
|
+
.map((w) => w.replace(/'/g, '')).filter((w) => w.length > 1 && !STOP.has(w))
|
|
81
|
+
.map((w) => w.slice(0, 5)));
|
|
82
|
+
const grams = (s) => {
|
|
83
|
+
const t = s.toLowerCase().replace(/[^a-z0-9 ]/g, '');
|
|
84
|
+
const g = new Set();
|
|
85
|
+
for (let i = 0; i + 3 <= t.length; i++) g.add(t.slice(i, i + 3));
|
|
86
|
+
return g;
|
|
87
|
+
};
|
|
88
|
+
const inter = (a, b) => { let n = 0; for (const x of a) if (b.has(x)) n++; return n; };
|
|
89
|
+
|
|
90
|
+
// Same thing said twice? Word overlap catches rewordings, letter trigrams catch
|
|
91
|
+
// typos ("dont comlicate that" vs "don't complicate it").
|
|
92
|
+
export function similar(a, b) {
|
|
93
|
+
const ta = a.t || tokens(a.s || a), tb = b.t || tokens(b.s || b);
|
|
94
|
+
const ga = a.g || grams(a.s || a), gb = b.g || grams(b.s || b);
|
|
95
|
+
const j = inter(ta, tb) / Math.max(1, new Set([...ta, ...tb]).size);
|
|
96
|
+
const d = (2 * inter(ga, gb)) / Math.max(1, ga.size + gb.size);
|
|
97
|
+
return Math.max(j, d);
|
|
98
|
+
}
|
|
99
|
+
export const SAME = 0.5;
|
|
100
|
+
|
|
101
|
+
// Greedy grouping against each group's first sentence. ponytail: O(n·groups);
|
|
102
|
+
// fine for a few thousand corrections, swap for an index if histories get huge.
|
|
103
|
+
export function group(items) {
|
|
104
|
+
const groups = [];
|
|
105
|
+
for (const it of items) {
|
|
106
|
+
const x = { ...it, t: tokens(it.s), g: grams(it.s) };
|
|
107
|
+
let best = null, score = 0;
|
|
108
|
+
for (const grp of groups) {
|
|
109
|
+
const sc = similar(grp.rep, x);
|
|
110
|
+
if (sc > score) { score = sc; best = grp; }
|
|
111
|
+
}
|
|
112
|
+
if (best && score >= SAME) best.items.push(x);
|
|
113
|
+
else groups.push({ rep: x, items: [x] });
|
|
114
|
+
}
|
|
115
|
+
return groups;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The wording made of the words you used most across a group, so a one-off
|
|
119
|
+
// typo ("pleae keep it simle") loses to the version you usually type.
|
|
120
|
+
function typical(items) {
|
|
121
|
+
const freq = new Map();
|
|
122
|
+
for (const i of items) for (const w of i.t) freq.set(w, (freq.get(w) || 0) + 1);
|
|
123
|
+
const score = (i) => [...i.t].reduce((n, w) => n + freq.get(w), 0) / Math.max(1, i.t.size);
|
|
124
|
+
return [...items].sort((a, b) => score(b) - score(a) || a.s.length - b.s.length)[0];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Repeats worth a rule: said at least `min` times, in at least two sessions.
|
|
128
|
+
export function repeats(groups, min = 3) {
|
|
129
|
+
// Greedy grouping can split one habit in two ("Don't complicate it" formed
|
|
130
|
+
// before "dont complex this" arrived). Merge groups whose best wordings match.
|
|
131
|
+
const merged = [];
|
|
132
|
+
for (const g of [...groups].sort((a, b) => b.items.length - a.items.length)) {
|
|
133
|
+
const home = merged.find((m) => similar(typical(m.items), typical(g.items)) >= SAME);
|
|
134
|
+
if (home) home.items.push(...g.items); else merged.push({ items: [...g.items] });
|
|
135
|
+
}
|
|
136
|
+
return merged
|
|
137
|
+
.filter((g) => g.items.length >= min && new Set(g.items.map((i) => i.session)).size >= 2)
|
|
138
|
+
.map((g) => {
|
|
139
|
+
const phrase = typical(g.items).s.replace(/[.!]+$/, '');
|
|
140
|
+
return {
|
|
141
|
+
phrase,
|
|
142
|
+
count: g.items.length,
|
|
143
|
+
sessions: new Set(g.items.map((i) => i.session)).size,
|
|
144
|
+
first: g.items.map((i) => i.ts).filter(Boolean).sort()[0] || '',
|
|
145
|
+
last: g.items.map((i) => i.ts).filter(Boolean).sort().at(-1) || '',
|
|
146
|
+
items: g.items,
|
|
147
|
+
};
|
|
148
|
+
})
|
|
149
|
+
.sort((a, b) => b.count - a.count);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const toRule = (phrase) => {
|
|
153
|
+
const p = phrase.trim().replace(/\s+/g, ' ');
|
|
154
|
+
return p.charAt(0).toUpperCase() + p.slice(1) + (/[.!]$/.test(p) ? '' : '.');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Is this already a line in the rule file? Then the wording isn't landing.
|
|
158
|
+
export function alreadyWritten(phrase, ruleText) {
|
|
159
|
+
return ruleText.split('\n').map((l) => l.replace(/^[\s>*\-#\d.]+/, '').trim())
|
|
160
|
+
.filter((l) => l.length > 3).some((l) => similar(phrase, l) >= SAME);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// How often a rule's phrase came back before and after it was added.
|
|
164
|
+
export function beforeAfter(rule, items) {
|
|
165
|
+
let before = 0, after = 0;
|
|
166
|
+
for (const i of items) {
|
|
167
|
+
if (similar(rule.text, i.s) < SAME) continue;
|
|
168
|
+
if (i.ts && i.ts >= rule.addedAt) after++; else before++;
|
|
169
|
+
}
|
|
170
|
+
return { before, after };
|
|
171
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toldya",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
-
"toldya": "bin/toldya.
|
|
6
|
+
"toldya": "bin/toldya.mjs"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin",
|
|
10
|
+
"lib",
|
|
10
11
|
"README.md",
|
|
11
12
|
"LICENSE"
|
|
12
13
|
],
|
|
@@ -29,5 +30,8 @@
|
|
|
29
30
|
"url": "git+https://github.com/singhlabsdev/toldya.git"
|
|
30
31
|
},
|
|
31
32
|
"author": "Singh Labs (Sevenopt Solutions Private Limited) <info@singhlabs.dev>",
|
|
32
|
-
"license": "MIT"
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
}
|
|
33
37
|
}
|
package/bin/toldya.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// toldya 0.0.1 — the name is reserved while v0.1 is built. This version only
|
|
3
|
-
// says what's coming, so nobody installs it expecting it to change a file.
|
|
4
|
-
|
|
5
|
-
const lines = [
|
|
6
|
-
'',
|
|
7
|
-
' toldya — stop repeating yourself to your AI.',
|
|
8
|
-
'',
|
|
9
|
-
' Coming in v0.1:',
|
|
10
|
-
' npx toldya read your Claude Code / Codex history, find what you',
|
|
11
|
-
' keep telling the AI, offer lines for CLAUDE.md',
|
|
12
|
-
' npx toldya owner/repo read a repo\'s PR review comments, find the repeats,',
|
|
13
|
-
' open one PR into AGENTS.md',
|
|
14
|
-
'',
|
|
15
|
-
' This version does nothing else. It reads nothing and writes nothing.',
|
|
16
|
-
' Follow along: https://github.com/singhlabsdev/toldya',
|
|
17
|
-
'',
|
|
18
|
-
];
|
|
19
|
-
process.stdout.write(lines.join('\n') + '\n');
|